make-figures
Generate publication-ready figures and visual abstracts for medical research papers. Supports ROC curves, forest plots, CONSORT/STARD/PRISMA flow diagrams, calibration plots, Kaplan-Meier curves, Bland-Altman plots, confusion matrices, pipeline diagrams, and journal-specific visu
Install
npx skills add https://github.com/Aperivue/medsci-skills/tree/main/skills/make-figures
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install aperivue-medsci-skills@llmmart
git clone https://github.com/Aperivue/medsci-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole aperivue/medsci-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Make-Figures Skill
You are helping a medical researcher generate publication-ready figures for medical research manuscripts. Every figure must meet journal specifications for dimensions, resolution, fonts, and color accessibility. Produce clean, data-focused visuals with no chartjunk.
Credits
The Critic Loop (Step 4b) in this skill is inspired by PaperBanana (Zhu et al., Automating Academic Illustration for AI Scientists, arXiv:2601.23265, 2025) and by prior self-refinement research — Self-Refine (Madaan et al., 2023), Reflexion (Shinn et al., 2023), and Constitutional AI (Anthropic, 2022). This is a clean-room reconstruction specialized for medical publication figures (STARD / CONSORT / PRISMA, journal-specific specs, Wong colorblind palette). No code, prompts, or configurations are derived from PaperBanana's repository.
Communication Rules
- Communicate with the user in their preferred language.
- All figure text (labels, legends, annotations) must be in English.
- Medical terminology is always in English.
Data Privacy Check
Before reading any data file, check whether it might contain Protected Health Information (PHI):
- If
*_deidentified.*files exist in the working directory, use those preferentially. - If only raw CSV/Excel files exist (no
*_deidentified.*counterpart), warn the user (ask in the user's preferred language):"Does this data contain patient identifiers (names, national ID / RRN, contact details, etc.)? If so, please de-identify it first with the
/deidentifyskill." - If the user confirms the data is already de-identified or contains no PHI, proceed.
Reference Files
- Figure specifications:
${CLAUDE_SKILL_DIR}/references/figure_specs.md - Figure style:
${CLAUDE_SKILL_DIR}/../analyze-stats/references/style/figure_style.mplstyle(or project's CLAUDE.md if available) - Project data: See CLAUDE.md for data locations under
2_Data/
Read figure_specs.md before generating any figure to confirm journal-specific requirements.
Journal AI-Image Policies (CRITICAL — check BEFORE generation)
Synced with the user's global rule
~/.claude/rules/journal-ai-image-policies.md. The table below is the local copy used during autonomous workflow; the global rule is authoritative when conflicts arise.
| Journal family | Policy on AI-generated images | Disclosure required |
|---|---|---|
| JACC family (incl. JACC: Asia, JACC Imaging, JACC EP, JACC BTS) | Prohibited without prior Editor-in-Chief permission (JACC pathway, PMC10167500) | Cover-letter pre-submission inquiry + ICMJE-style declaration |
| NEJM | AI image generation prohibited | N/A |
| Radiology / Radiology AI | Allowed with disclosure | Manuscript disclosure block |
| Nature family | Allowed with disclosure + license check | Methods + figure legend |
| Lancet family | Disclosure required, generation discouraged | Manuscript disclosure |
| Default (target unknown) | Treat as prohibited until confirmed | N/A |
Hard rule: For JACC, NEJM, or any "unknown" target journal, never use Gemini / DALL-E / Midjourney / Stable Diffusion / Nano Banana to create images that will appear in figures, Central Illustrations, or graphical abstracts. AI text-editing of the manuscript prose remains acceptable subject to standard disclosure.
Default workflow when AI images are not allowed
- SMART Servier Medical Art — https://smart.servier.com/, CC BY 4.0, free, 3,000+ vector medical icons (anatomy, organs, ethnicity-specific human figures, drugs, devices). Commercial / journal use allowed. Required attribution (1 line in figure legend OR methods):
Anatomical icons modified from SMART Servier Medical Art (CC BY 4.0).
- NIAID BioArt (https://bioart.niaid.nih.gov) — public domain (US Govt), microbiology / immunology / lab-tech focus.
- BioRender (https://www.biorender.com) — institutional license usually required; use the exported "Publication-ready" PNG/TIFF and cite per BioRender publication policy.
- For "diseased" variants not directly available (e.g., calcified vessel from a clean vessel): reuse the healthy asset and overlay disease markers via matplotlib
scatter/Circle/PathPatch. Keeps the entire pipeline non-AI and reproducible.
Asset directory convention
manuscript/figures/_assets_servier/ # CC BY 4.0 source PNGs
manuscript/figures/_assets_servier/CITATION.md # source URL + download date per asset
manuscript/figures/_assets_data/ # data-driven raster (R / matplotlib heat maps, KM, etc.)
manuscript/figures/_legacy/ # archived prior versions
Composition scripts should load only from _assets_servier/ and _assets_data/. If a script imports from _assets_ai/, treat it as a policy violation for JACC/NEJM/unknown targets.
When a figure is produced by a data-driven .py/.R script (ROC, forest, KM, calibration, heat maps), lint that script before finalizing with the /analyze-stats code-quality gate (check_generated_code.py {script} --strict): it catches a missing plotting seed for any bootstrapped CI band, a hardcoded absolute data path, or a hand-typed data literal that should have been read from the analysis CSV.
Decoration vs information
Even when AI images are allowed, AI-generated illustrations are immediately recognizable to experienced reviewers (small decorative icons that add no information, overly uniform layouts, generic clip-art style). For high-impact submissions, prefer Servier / BioArt / BioRender + matplotlib overlays over AI.
DPI and Resolution Guide
| Output | Minimum DPI | Notes |
|---|---|---|
| Journal halftone (photos, screenshots) | 300 | Standard for most journals |
| Journal line art (diagrams, graphs) | 600 | Required by Radiology, most Elsevier journals |
| Poster presentation | 150-200 | Lower is acceptable for large-format prints |
| Screen/web only | 72-150 | Not for print submission |
Practical workflow for screen captures:
- Use HyperSnap or similar tool with DPI pre-set to the journal requirement
- Compose the figure in PPT at high zoom → capture at target DPI → save as TIFF/PNG
- Verify final file dimensions match journal column width requirements
Visual Abstract / Graphical Abstract
Many journals now require or strongly encourage visual abstracts. European Radiology made graphical abstracts mandatory for all Original Articles from first revision (Jan 2025). Submitting one voluntarily signals effort and can improve editorial impression.
Journal Requirements
| Status | Example Journals |
|---|---|
| Mandatory | European Radiology (from 1st revision, all Original Articles) |
| Encouraged | Abdominal Radiology, JCO, Annals of Internal Medicine |
| Voluntary | Most other journals — improves social media visibility |
Check the target journal profile (write-paper/references/journal_profiles/) for specific
visual abstract requirements before starting.
Workflow
- Check journal template. Look for an official PPTX template in
${CLAUDE_SKILL_DIR}/references/visual_abstract_templates/{journal}.pptx. If no journal-specific template exists, usemedsci_default.pptx. - Extract content from the manuscript:
- Title: Full article title
- Hypothesis/Question: Derived from Key Point 1 or study objective (max 1 sentence)
- Methodology: Brief flowchart or ≤3 bullets, <6 words each
- Visual element: Study's own figure (ROC curve, flow diagram, representative image)
- Badges: Patient cohort (N=...) | Modality/organ | Single/Multi-center
- Main finding: Derived from Key Point 3 (<20 words)
- Citation: Journal (year) Authors; DOI
- Select visual element (priority order — no API needed for top options):
- Study's own figures (ROC, flow diagram, representative image) — always preferred
- Free illustration from Servier Medical Art or NIAID BioArt
(see
${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md) - Manual drawing in PPT/Keynote/Figma
- AI generation via
generate_image.py --style medical(only if GEMINI_API_KEY set)
- Generate using the script:
python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \ --template medsci_default \ --title "Article Title" \ --hypothesis "Research question" \ --methods "Method 1|Method 2|Method 3" \ --finding "Main finding statement" \ --citation "Eur Radiol (2026) Author A et al; DOI:..." \ --visual figures/fig1_roc_curve.png \ --badges "N=450|CT chest|Multi-center" \ --output figures/visual_abstract.pptx - Review with user. Open the PPTX to verify layout and content. Iterate.
- Export. PPTX is the primary deliverable. For PNG: open in PowerPoint/Keynote → export,
or use LibreOffice CLI (
soffice --headless --convert-to png).
Design Principles
- One page, landscape (16:9) or per journal template specification
- Three sections: Study question → Key method → Main result
- Use the study's actual figures rather than generic graphics
- Minimize text — let visuals carry the message
- Every visual element must serve a purpose (no decorative clip-art)
Available Templates
| Template | File | Use When |
|---|---|---|
| MedSci Default | medsci_default.pptx |
Any journal without an official template |
| JACC Central Illustration | jacc_central_illustration.pptx |
JACC family journals (use --type central-illustration) |
Using a journal's own template. Several journals publish one — European Radiology requires a
graphical abstract from first revision and supplies EURA-GA-Jan2025.pptx. We do not redistribute
them: a template you may download is not a template we may ship. Use yours directly instead:
python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \
--template /absolute/path/to/EURA-GA-Jan2025.pptx ...
--template takes an absolute path to any .pptx. The script locates the fields by their text
content rather than by shape name, so a journal's own template works unmodified. If the path does
not exist it falls back to medsci_default.pptx.
To add a new journal template: see ${CLAUDE_SKILL_DIR}/references/visual_abstract_templates/template_guide.md.
Central Illustration vs Visual Abstract
A Central Illustration (CI) is not a Visual Abstract (VA). They serve different purposes and follow different rules. JACC family journals (JACC, JACC: Asia, JACC: Cardiovascular Imaging, JACC: Heart Failure, JACC: CardioOncology, JACC: Clinical Electrophysiology, JACC: Basic to Translational Science) require a Central Illustration with every Original Article. Reference: Fuster V, Mann DL. JACC. 2019;74(22):2816–2820.
| Aspect | Central Illustration | Visual Abstract |
|---|---|---|
| Purpose | Single key finding / take-home message | Methods + Results pictorial summary |
| Where in paper | End of Results / start of Discussion | Beginning of paper |
| Methods content | None | Required |
| Audience | Cardiovascular clinicians + journal-issue readers | Broad including non-specialists / social media |
| Used by | All JACC family + JACC: Asia | Originally JACC: Basic to Translational Science |
| Text density | Minimal (graphical priority) | More allowed (methods labels) |
| Bar graphs | OK if they capture entire message | Avoid — use ↑↓ arrows |
| Default complexity | 1–3 visual zones | Q→M→R three blocks |
Fuster-Mann five rules (CI must pass all)
- Know the message. One finding, not study design + multiple findings.
- Convey graphically, not textually. Even a simple KM curve is OK.
- Avoid using too much text. Replace with icons or arrows.
- Avoid secondary messages. ≤ 5 seconds for a viewer to state the main finding.
- Simplicity is superior. Default to fewer panels.
Full guidance and validation thresholds: ${CLAUDE_SKILL_DIR}/references/jacc_central_illustration_principles.md.
CI mode invocation
python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \
--type central-illustration \
--visual figures/central_illustration_v2.png \
--citation "FirstAuthor Last et al. Journal Name 2026; vol(issue):pages." \
--output submission/jacc_asia/central_illustration.pptx \
--ci-zones 3 --ci-label-words 22 --ci-numerical-points 2 \
--ci-raw-text "warranty drops to 3 years in age 45+ with cardiometabolic burden; MASLD HR 1.77"
CI mode validates before rendering and rejects (exit 2) if any of: zones > 3, label words > 30, numerical points > 4, or methodology terms (cohort flow / inclusion / exclusion / study design / enrollment / randomized / sample size / CONSORT / PRISMA / STARD) appear in --ci-raw-text. Override individual rules with --ci-allow {zones|words|numerical|methods} only when you have a defensible reason.
The JACC submission PPTX is a 10×7.5 in slide with 4 placeholders (citation textbox, content picture, footer textbox reserved, JACC logo). The red border + blue "CENTRAL ILLUSTRATION:" header are applied by JACC editorial after acceptance — authors submit only the content figure + citation.
Workflow
Step 1: Specify
Before specifying figure type, read ${CLAUDE_SKILL_DIR}/references/design_principles.md —
identify (1) the one-sentence key message, (2) audience and reading-time budget, and
(3) whether a figure is the right vehicle (vs a small table or in-line text). The
five strategies in that file shift Step 1 from "which chart fits the data" to
"what should the reader remember 10 seconds later." Skip only when the figure
is mandated by a reporting guideline (e.g., PRISMA / CONSORT flow), and even
then apply the cognitive-load checklist.
For reporting-guideline figures, also load
${CLAUDE_SKILL_DIR}/references/reporting_guideline_figure_map.md — the
14-row table tells you which guideline mandates which figures and whether
this skill ships an official template (✅), generic flow only (⚠️), or
needs manual production (❌). Critical for AI-extension guidelines
(CONSORT-AI, STARD-AI, TRIPOD+AI, CLAIM 2024, DECIDE-AI).
For medical AI / engineering pipeline figures (DICOM workflow,
annotation pipeline, federated learning topology, model architecture),
also load ${CLAUDE_SKILL_DIR}/references/pipeline_concepts_medical_ai.md —
canonical layouts, required annotations, and tool selection per type.
Optional flags:
--study-type <type>: One of:diagnostic-accuracy,ai-validation,meta-analysis,dta-meta-analysis,observational-cohort,rct,case-report. When set, auto-generate the full figure set from the Study-Type Figure Sets table below without prompting for individual figure types.--data-dir <path>: Directory containing analysis outputs (CSVs,_analysis_outputs.md). Default: current working directory.
Ask the user for:
- Figure type (from the supported types below) — skipped when
--study-typeis provided - Data source (file path, DataFrame, or manual values)
- Target journal (for dimension/font requirements)
- Panel layout (single panel, multi-panel, or let you decide)
- Any special requests (annotations, highlights, reference lines)
- Study type (if not passed via
--study-type): determines the required figure set
If the user provides enough context, infer missing parameters and confirm before proceeding.
Step 2: Configure
- Load the figure style file:
import matplotlib.pyplot as plt import os style_path = os.path.join(os.environ.get('CLAUDE_SKILL_DIR', '.'), '../analyze-stats/references/style/figure_style.mplstyle') if os.path.exists(style_path): plt.style.use(style_path) - Look up journal-specific dimensions from
${CLAUDE_SKILL_DIR}/references/figure_specs.md. - Set the colorblind-safe palette (Wong palette by default).
- Configure font sizes per element type (title, axis label, tick label, legend, annotation).
Step 3: Generate
Create the figure using Python (matplotlib/seaborn as primary, with specialized libraries as needed).
Script structure:
"""
Figure: {description}
Date: {YYYY-MM-DD}
Target: {journal}
Dimensions: {width} x {height} inches @ {DPI} DPI
"""
import numpy as np
import matplotlib.pyplot as plt
import os
style_path = os.path.join(os.environ.get('CLAUDE_SKILL_DIR', '.'), '../analyze-stats/references/style/figure_style.mplstyle')
if os.path.exists(style_path):
plt.style.use(style_path)
# Wong colorblind-safe palette
WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73',
'#F0E442', '#0072B2', '#D55E00', '#CC79A7']
np.random.seed(42)
Step 4: Review
Present the figure to the user and ask:
- Does the layout work?
- Are labels and annotations correct?
- Any adjustments to colors, sizing, or emphasis?
Iterate until the user approves.
Step 4b: Critic Loop (self-critique before final export)
Before Step 5 Export, run the automated Critic Loop. This is two stages — deterministic quantitative checks via Python, then qualitative review by Claude itself — and the combined output tells us whether to re-render or hand off to the user.
Stage 1: Quantitative checks (critic_figure.py)
python ${CLAUDE_SKILL_DIR}/scripts/critic_figure.py \
figures/fig1_stard.png \
--type stard \
--spec-min-dpi 600 \
--spec-width-in 7.0 \
--source-text figures/fig1_stard.txt \ # optional: expected strings for OCR coverage
--out figures/fig1_stard.critique.json
This produces a JSON report covering:
- DPI and physical width vs. journal spec
- Dominant-color breakdown and out-of-Wong-palette fraction
- OCR-detected word count, minimum text height, and (if a source-text file was provided) source-word coverage
Stage 2: Qualitative review (Claude session)
- Use the Read tool to load the generated PNG.
- Read the corresponding rubric file:
- Flow diagrams:
${CLAUDE_SKILL_DIR}/references/critic_rubrics/flow_diagram.md(sections A–G; section G adds cognitive-load and template-fidelity checks) - Data plots:
${CLAUDE_SKILL_DIR}/references/critic_rubrics/data_plot.md(sections A–G; section G adds calibration / fairness / colorblind+redundant / dataset-flow / decision-curve checks for medical AI papers) - For PRISMA / CONSORT / STARD / STROBE specifically, also read
${CLAUDE_SKILL_DIR}/references/flow_diagram_lessons.md— five production lessons covering official-template fidelity, PDF export fidelity (VML fallback), docx XML escape, sequential placeholder mapping, and frozen-version sync with the manuscript. - For AI-extension guidelines (CONSORT-AI, STARD-AI, TRIPOD+AI,
CLAIM 2024, DECIDE-AI), also read
${CLAUDE_SKILL_DIR}/references/reporting_guideline_figure_map.md— the row for the target guideline lists mandatory figures and which ones this skill cannot template (production path documented per row). - For medical-AI pipeline / DICOM / federated / architecture figures,
also read
${CLAUDE_SKILL_DIR}/references/pipeline_concepts_medical_ai.md.
- Flow diagrams:
- Read the
_why.mddesign notes in${CLAUDE_SKILL_DIR}/references/exemplar_diagrams/{type}/— hierarchy, whitespace, typography, emphasis, colour. They are the anchors. Where a rendered exemplar is bundled (template_output*.png, produced by this skill's own R script), Read 1–2 of those too; the figures cropped from published papers were removed in 2026-07 because an MIT-licensed package cannot redistribute them (see that directory's README). If you have your own exemplars locally, point the loop at them — they stay on your machine. For a non-flow data plot (forest, ROC, KM, calibration), read the matching anatomy model in${CLAUDE_SKILL_DIR}/references/exemplar_plots/(e.g.,forest_plot.md). - Score every rubric item as PASS / PARTIAL / FAIL with a one-line note, using the format at the bottom of the rubric file.
- Emit a "Required edits before next render" list of concrete source-code changes (D2 node renames, count corrections, matplotlib parameter tweaks).
Refinement loop
- If all items are PASS → proceed to Step 5 Export with
critic_pass: yes. - If any item is FAIL → apply the required edits to the source (D2 file or matplotlib script), re-render, and re-run Stage 1 + Stage 2. Default maximum is T=2 rounds; the user may request up to T=3.
- If after the max rounds some items remain PARTIAL, proceed with
critic_pass: partialand record the residual items in the manifest'scritic_notesfield.
Record the final state in _figure_manifest.md (see the manifest format
below) so downstream steps (/write-paper Phase 2 embedding and Phase 7
DOCX build) and future critic passes can see the history.
Step 5: Export
Save final outputs:
- PDF (vector format, preferred for journal submission)
- PNG (300 DPI raster, for review and presentation)
- TIFF (if the journal requires it, 300 DPI LZW compression)
Name files descriptively: fig1_roc_curve.pdf, fig2_consort_flow.pdf, etc.
For PPTX outputs (visual abstract, central illustration, or any deck the figure
will live in): run the Mac-compatibility validator before delivery. PowerPoint
Mac silently drops TIFF, renders <a:sp3d> 3-D bevels as red outlines that PDF
export does not show, and refuses to open files whose app.xml slide count
disagrees with the actual slide XML files. This script catches all four classes
of defect codified in ~/.claude/rules/pptx-mac-compatibility.md:
python ${CLAUDE_SKILL_DIR}/scripts/validate_pptx_mac_compat.py \
figures/visual_abstract.pptx \
--json figures/visual_abstract.mac_compat.json \
--strict
Exit code 1 means at least one FAIL — fix per the fix: field in the JSON
report and re-render the PPTX before delivery. Exit code 0 with WARN is
acceptable. Skip this step when the figure is PNG/PDF only (no PPTX).
Step 6: Design QC Checklist
Before delivering the final figure, verify all items:
- Font: Sans-serif (Arial/Helvetica), minimum 7pt, axis labels ≥ 9pt
- Color: Wong/Okabe-Ito colorblind-safe palette used
- Colorblind test: Would the figure work for deuteranopia? (no red-green only distinctions)
- Grayscale test: Information preserved when printed in black & white
- Alignment: All elements on a consistent grid; panels aligned
- Vector output: PDF/SVG saved (not just PNG)
- Resolution: ≥ 300 DPI for raster elements, ≥ 600 DPI for line art
- Journal specs: Dimensions, font, and format match target journal requirements
- No chartjunk: No 3D effects, unnecessary gridlines, gradient fills, or decorative elements
- Caption: Drafted with key finding, abbreviations, statistical details, and sample size
Study-Type Figure Sets
When the study type is known (from /write-paper Phase 0 or user specification), auto-detect and generate the complete required figure set without asking for each figure individually.
| Study Type (Guideline) | Required Figures |
|---|---|
| Diagnostic accuracy (STARD) | STARD flow diagram, ROC curve, confusion matrix, calibration plot |
| AI validation (TRIPOD+AI / CLAIM) | Flow diagram, ROC curve, confusion matrix, calibration plot, feature importance or SHAP, Grad-CAM (if imaging) |
| Meta-analysis (PRISMA) | PRISMA flow diagram, forest plot, funnel plot |
| DTA meta-analysis (PRISMA-DTA) | PRISMA flow diagram, paired forest plot (Se + Sp), SROC curve, Deeks funnel plot |
| Observational cohort (STROBE) | Flow diagram, Kaplan-Meier curves (if survival endpoint) |
| RCT (CONSORT) | CONSORT flow diagram, primary endpoint figure |
| Case report / series (CARE) | Clinical timeline figure (exemplar_plots/clinical_timeline.md), annotated multimodality imaging panel when visually load-bearing (exemplar_plots/imaging_panel.md); for a series, an all-cases summary table |
The manifest is mandatory. After generating all figures, write
figures/_figure_manifest.md — one row per figure (Figure | Path | Type | Tool | Critic | Rounds | Description) plus a ## Critic notes section recording any residual PARTIAL items and
why they were accepted. It is consumed by /write-paper Phase 2 (figure embedding) and Phase 7
(DOCX build); verify it exists and is non-empty before finishing. Format and field definitions:
${CLAUDE_SKILL_DIR}/references/figure_manifest.md.
Flow diagram generation rule. STARD / CONSORT / PRISMA / STROBE flow diagrams MUST use the
standardized R pipeline scripts/generate_flow_diagram.R (DiagrammeR + Graphviz dot + rsvg) — the
single canonical tool for all four. Do NOT use matplotlib FancyBboxPatch (manual coordinates
break when text changes, and patches distort when embedded in DOCX). Do NOT use D2 for new
flow diagrams (weak font control, overlap needs manual post-processing). Numbers in labels must be
CSV-derived, or hand-written only when the value lives in a commit-tracked data artifact.
Read on demand:
| File | Read it when | Cost if read blindly |
|---|---|---|
references/flow_diagram_recipe.md |
you are generating a STARD / CONSORT / PRISMA / STROBE flow diagram | ~2,200 tokens — a ROC curve or forest plot needs none of it |
references/figure_manifest.md |
you are writing _figure_manifest.md |
~700 tokens of output format |
Tool Selection Guide
Choose the right tool for each figure type. Using matplotlib for flow diagrams leads to hard-coded coordinates that break when text changes — use auto-layout tools instead.
Data Visualization → matplotlib/seaborn (this skill)
Best for figures where data drives the layout. This skill handles these directly:
| Type | Use Case | Key Library |
|---|---|---|
| ROC Curve | Diagnostic accuracy | matplotlib, sklearn |
| Forest Plot | Meta-analysis | matplotlib |
| Calibration Plot | Prediction model | matplotlib |
| KM Curve | Survival analysis | lifelines, matplotlib |
| Bland-Altman | Agreement | matplotlib |
| Confusion Matrix | Classification | seaborn |
| Box/Violin Plot | Group comparison | seaborn |
| Bar Chart | Categorical comparison | matplotlib |
| Heatmap | Correlation/agreement | seaborn |
Flow Diagrams → Dedicated Tools (NOT matplotlib)
Flow diagrams require auto-layout engines. Do NOT use matplotlib patches with manual coordinates — this causes the "absolute coordinate hell" problem where changing one box breaks all downstream positions.
| Type | Recommended Tool | Why |
|---|---|---|
| STROBE (cohort / cross-sectional) | scripts/generate_flow_diagram.R --type strobe |
Single canonical tool; auto-layout; vector PDF + 300/600 dpi PNG |
| CONSORT (RCT) | scripts/generate_flow_diagram.R --type consort |
Same pipeline; monochrome Arial default |
| PRISMA 2020 (SR/MA) | scripts/generate_flow_diagram.R --type prisma |
Faithfully implements PRISMA 2020 structure; avoids PRISMA2020 R package's webshot-based raster PDF issue |
| STARD (DTA) | scripts/generate_flow_diagram.R --type stard |
Same pipeline; supports 2x2 reference-standard split |
| Pipeline Diagram | D2 (legacy) | Until pipeline-diagram support is added to the R script |
R workflow for flow diagrams: See the "R flow diagram recipe" above in the Flow diagram generation rule. Key points: YAML config → Rscript scripts/generate_flow_diagram.R --type <t> --config <yaml> --out <prefix> → PDF + 300/600 dpi PNG. Templates in references/exemplar_diagrams/{strobe,consort,prisma,stard}/template_input.yaml.
Official Reporting Guideline Templates → templates/official/
When a journal requires the canonical, statement-issued template (rather than
the auto-laid-out R version), use the bundled official files in
templates/official/{prisma2020,consort2010,stard2015,spirit2013}/.
| Guideline | What ships | When to use |
|---|---|---|
| PRISMA 2020 | Locally built .pptx (4 variants) + fill_prisma_template.py |
Reviewer asks for the official PRISMA 2020 layout, or you want editable PowerPoint instead of an R-rendered PDF. |
| STROBE (cohort) | Parametric .pptx builder build_strobe_template.py (single-script, takes YAML config) |
Cohort/case-control study Figure 1 when co-authors want PowerPoint they can hand-edit. Auto-fits text, content-fits slide, dashed-border exclusion side-branches with strictly-horizontal connectors. Optional left-side phase column (omit stages: for the plain STROBE convention; include it for the PRISMA-style Identification/Screening/Inclusion/Analysis column). Pair with generate_flow_diagram.R --type strobe for the vector PDF/TIFF submission file. |
| CONSORT 2025 | Official .docx flow diagram + checklist |
RCT submissions to journals that mandate the consort-spirit.org template. |
| STARD 2015 | Official .pdf flow diagram + .docx checklist |
Diagnostic accuracy studies; flow diagram is fixed PDF, checklist is editable. |
| SPIRIT 2025 | Official .docx participant timeline + checklist |
Trial protocols. |
Refresh / fill workflow:
# Refresh from canonical sources (CC-BY 4.0 / public-statement licenses)
bash ${CLAUDE_SKILL_DIR}/scripts/fetch_official_templates.sh
# Build PRISMA 2020 .pptx (one-time; site blocks programmatic .docx fetch)
python3 ${CLAUDE_SKILL_DIR}/scripts/build_prisma2020_template.py \
--variant new \
--out ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx
# Fill counts — positional 10-tuple matching most SR/MA workflows:
# n_db, n_dup, n_screened, n_screen_excluded,
# n_sought, n_assessed, n_excl_r1, n_excl_r2, n_excl_r3, n_studies
python3 ${CLAUDE_SKILL_DIR}/scripts/fill_prisma_template.py \
--template ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \
--counts "315,122,186,7,111,204,102,84,3,15" \
--out fig1_prisma_filled.pptx
# Or use full JSON mapping for studies with non-standard PRISMA splits
python3 ${CLAUDE_SKILL_DIR}/scripts/fill_prisma_template.py \
--template ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \
--counts-file my_counts.json \
--out fig1_prisma_filled.pptx
# STROBE — parametric single-script builder (cohort study; spine structure varies per study).
# YAML schema: stages, spine (id/stage/text), exclusions (after/text). Consecutive same-stage
# rows share one phase label automatically. Stage box fills auto-pick readable text color.
python3 ${CLAUDE_SKILL_DIR}/scripts/build_strobe_template.py \
--config figures/figure1_strobe.yaml \
--out figures/figure1_strobe.pptx
The builder checks that the exclusion cascade closes — the count in a spine box, minus the
exclusions declared after it, must equal the next spine box (A - Σ(exclusions after A) == B), for every link that declares an exclusion. It warns loudly on any imbalance and, with
--strict-cascade, refuses to build. This catches the figure-image arithmetic drift that
text-grep and prose gates miss (a dropped exclusion leaving the figure short of the analytic
N). Run scripts/_strobe_cascade.py --config figure1_strobe.yaml --strict to check a config
without rebuilding the diagram.
For STROBE the canonical KJR/Radiology/BMJ submission flow is:
- Render the vector submission file via the auto-fitting Graphviz path:
Rscript ${CLAUDE_SKILL_DIR}/scripts/generate_flow_diagram.R --type strobe --config figures/figure1_strobe_graphviz.yaml --out figures/figure1 - Build the editable PowerPoint companion via
build_strobe_template.pyso co-authors and senior reviewers can adjust prose/positioning before sign-off. - Re-export the final PPTX to PDF/TIFF only after co-author edits are integrated.
See templates/official/NOTES.md for licenses, attribution, and refresh notes.
Visual / Graphical Abstracts → python-pptx Template Generator
| Type | Recommended Tool |
|---|---|
| Visual Abstract (any journal) | generate_visual_abstract.py with PPTX template |
| Visual element illustration | Study's own figures (preferred), or free libraries (Servier/NIAID) |
| Medical Illustration | See ${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md |
See the Visual Abstract section above for the full workflow.
Hybrid Workflow (recommended for publication)
Data plots: matplotlib/seaborn → PDF + PNG (this skill)
Flow diagrams: generate_flow_diagram.R (DiagrammeR + rsvg) → PDF + 300/600 dpi PNG
Final assembly: pandoc or python-docx (auto-embedded in DOCX)
Supported Figure Types (matplotlib/seaborn)
| Type | Use Case | Key Library | Output |
|---|---|---|---|
| ROC Curve | Diagnostic accuracy | matplotlib, sklearn | Single/multi-model ROC with AUC |
| Forest Plot | Meta-analysis | matplotlib | Effect sizes with CIs, diamond summary |
| Calibration Plot | Prediction model | matplotlib | Observed vs predicted with Hosmer-Lemeshow |
| KM Curve | Survival analysis | lifelines, matplotlib | With risk table, log-rank p |
| Bland-Altman | Agreement | matplotlib | With mean diff, +/-1.96 SD limits |
| Confusion Matrix | Classification | seaborn | Heatmap with percentages |
| Box/Violin Plot | Group comparison | seaborn | With individual data points |
| Pipeline Diagram | Methods figure | D2 (preferred) or matplotlib | Processing/workflow steps |
| Bar Chart | Categorical comparison | matplotlib | With error bars (CI or SD) |
| Heatmap | Correlation/agreement | seaborn | Color-coded matrix |
Figure Type Templates
ROC Curve
from sklearn.metrics import roc_curve, auc
fig, ax = plt.subplots(figsize=(3.5, 3.5))
fpr, tpr, _ = roc_curve(y_true, y_score)
roc_auc = auc(fpr, tpr)
ax.plot(fpr, tpr, color=WONG[5], lw=1.5,
label=f'Model (AUC = {roc_auc:.3f})')
ax.plot([0, 1], [0, 1], 'k--', lw=0.8, alpha=0.5)
ax.set(xlabel='1 - Specificity', ylabel='Sensitivity',
xlim=[-0.02, 1.02], ylim=[-0.02, 1.02])
ax.legend(loc='lower right', frameon=False)
- For multiple models: use distinct Wong palette colors, include AUC + 95% CI in legend.
- For comparison: report DeLong p-value in annotation.
Forest Plot
- Horizontal layout: effect sizes as squares (sized by weight), CIs as lines.
- Diamond at bottom for pooled estimate.
- Vertical dashed line at null effect (OR=1 or MD=0).
- Axis label: "Favours A | Favours B" or appropriate.
- Include heterogeneity stats (I-squared, p) below the diamond.
Flow Diagrams (STROBE / CONSORT / PRISMA / STARD)
Single canonical tool: scripts/generate_flow_diagram.R (see the R flow diagram recipe above). Do not fall back to matplotlib for flow diagrams — manual coordinates break when text changes and patches distort in DOCX. D2 remains a documented legacy fallback only when R is unavailable.
Layout invariants:
- Rectangular boxes with rounded corners for stages; notes (
shape: note) for exclusion side-boxes. - Vertical top-down flow by default; horizontal only when the manuscript layout demands it.
- Every box label contains the count (e.g.,
"Assessed for eligibility\n(n = 450)"). - Numbers are CSV-derived (numerical-safety) — author the YAML from an R/Python script that reads the upstream data, or cite the source file in a comment when a literal value is unavoidable.
- Follow the official template layout from each guideline.
- Use relative positioning — never hard-code absolute y-coordinates. Calculate each box position from the previous box's bottom edge plus a consistent gap constant.
- Define gap constants at the top of the script (e.g.,
GAP_SMALL = 1.5,GAP_BRANCH = 2.2). - Avoid magic number padding in arrow endpoints — use named constants.
D2 approach (legacy fallback — use only when R is unavailable; the R script above is canonical):
d2 --layout elk --theme 0 flow.d2 output.svg
# Then: open SVG in Figma → grid-snap → font swap → export PDF
Caption ↔ flow-SSOT reconciliation (before Step 5 Export). The flow-diagram config (the YAML/script that generate_flow_diagram.R consumes) is the single source of truth for participant counts. A hand-written Figure 1 caption drifts from it whenever the cohort is re-locked but the caption is not — the classic "caption says n = 1,284 analytic, diagram box says n = 998" defect, which surfaces only at submission. Re-derive the caption counts from the flow config and reconcile:
python3 ${CLAUDE_SKILL_DIR}/scripts/derive_figure_legend_counts.py \
--flow-config figures/figure1_strobe_graphviz.yaml \
--manuscript manuscript/index.qmd \
--out qc/figure_legend_counts.json --strict
Any n = N in the caption that is not a box count in the flow config is a MISMATCH (stale caption) — update the caption from the config, never the reverse. This pairs with numerical-safety's "re-derive prose counts every revision" rule and with /sync-submission's cross-document N checks. (The reconciler is stdlib-only and parses the config as text, so it works regardless of the flow tool.)
Calibration Plot
- 45-degree reference line (perfect calibration).
- Grouped observed vs predicted with error bars.
- Report Hosmer-Lemeshow statistic and Brier score in annotation.
- Optional: histogram of predicted probabilities at the bottom.
Kaplan-Meier Curve
- Step function with distinct colors per group.
- Censoring marks as small vertical ticks.
- Number-at-risk table below the plot (aligned with x-axis ticks).
- Log-rank p-value in annotation.
- Median survival with 95% CI if applicable.
Bland-Altman Plot
- X-axis: mean of two measurements.
- Y-axis: difference between measurements.
- Horizontal lines: mean difference (solid), +/-1.96 SD (dashed).
- Annotate the mean diff and limits of agreement values.
- Optional: proportional bias check (regression line through points).
Confusion Matrix
- Heatmap with both counts and percentages in each cell.
- Row-normalized percentages preferred (sensitivity per class).
- Clear axis labels: "Predicted" (x) and "Actual" (y).
- Use sequential colormap (Blues or Greens), not diverging.
Box/Violin Plot
- Show individual data points (jittered) overlaid on box or violin.
- Mark median and mean distinctly.
- Statistical annotation brackets with significance stars.
- Stars: * p<0.05, ** p<0.01, *** p<0.001, ns for non-significant.
Pipeline Diagram
- Horizontal or vertical flow of processing stages.
- Boxes: rounded rectangles with stage name and brief description.
- Arrows: labeled with data counts or transformation type.
- Color-code stages by category (data collection, processing, validation).
- Keep text minimal; use supplementary caption for details.
Bar Chart
- Error bars: 95% CI (preferred) or SD, stated in caption.
- Individual data points overlaid if n < 30.
- Horizontal orientation for many categories.
- Sort by value (descending) unless order is meaningful.
Heatmap
- Annotate cells with values.
- Use sequential colormap for correlation (coolwarm diverging if centered at zero).
- Mask diagonal for correlation matrices.
- Cluster rows/columns if appropriate.
Style Rules
Colors
Wong colorblind-safe palette (default):
WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73',
'#F0E442', '#0072B2', '#D55E00', '#CC79A7']
Sequential palettes (for heatmaps):
- Positive values:
BluesorGreens - Diverging (centered at 0):
coolwarmorRdBu_r - Agreement matrices:
YlOrRd
Rules:
- Never use red-green only distinctions.
- Use line style (solid, dashed, dotted) in addition to color for line plots.
- Use marker shape in addition to color for scatter plots.
Typography
| Element | Font Size | Weight |
|---|---|---|
| Figure title (if any) | 10 pt | Bold |
| Axis label | 9 pt | Regular |
| Tick label | 8 pt | Regular |
| Legend text | 8 pt | Regular |
| Annotation | 8 pt | Regular |
| Panel label (A, B, C) | 12 pt | Bold |
- Font family: Arial or Helvetica (sans-serif).
- Panel labels: uppercase bold letter, top-left of each panel.
Layout
- Minimize white space while maintaining readability.
- Align multi-panel figures on a grid.
- Consistent axis ranges across comparable panels.
- No figure titles in the plot itself (title goes in the caption below).
Statistical Annotations
- Significance stars: * p<0.05, ** p<0.01, *** p<0.001
- Place above comparison brackets.
- Report exact p-value in the figure legend or caption, not in the plot.
- For AUC, correlation, or agreement: display in the legend with 95% CI.
Journal Specifications
Default dimensions (override from figure_specs.md if journal-specific):
- Single column: 3.5 in (88 mm) width
- 1.5 column: 5.0 in (127 mm) width
- Double column: 7.0 in (178 mm) width
- Full page: 7.0 x 9.5 in (178 x 241 mm)
- DPI: 300 minimum for halftone, 600 for line art
- File formats: PDF (vector, preferred) + PNG (300 DPI)
- No chartjunk: no 3D effects, no unnecessary gridlines, no decorative elements, no gradient fills
Multi-Panel Figures
For composite figures with multiple panels:
fig, axes = plt.subplots(nrows, ncols, figsize=(width, height))
# Label each panel
for ax, label in zip(axes.flat, 'ABCDEFGH'):
ax.text(-0.15, 1.05, label, transform=ax.transAxes,
fontsize=12, fontweight='bold', va='top')
Common layouts:
- 2-panel horizontal:
figsize=(7.0, 3.5), 1 row x 2 cols - 2-panel vertical:
figsize=(3.5, 7.0), 2 rows x 1 col - 2x2 grid:
figsize=(7.0, 7.0), 2 rows x 2 cols - 3-panel:
figsize=(7.0, 3.0), 1 row x 3 cols
Use plt.tight_layout() or fig.subplots_adjust() for spacing.
Caption Writing
After generating each figure, draft a caption following these rules:
- First sentence: Describe what the figure shows (type + key finding).
- Subsequent sentences: Define abbreviations, explain symbols, state sample sizes.
- Statistical details: Note the test used and significance threshold.
- Format: "Figure . " -- no bold, no title case.
Example:
Figure 1. Receiver operating characteristic curves comparing the diagnostic performance of the multi-agent pipeline (blue) and single-agent baseline (orange) for identifying incorrect Anki flashcard content. The area under the curve was 0.92 (95% CI: 0.89-0.95) for the multi-agent pipeline and 0.84 (95% CI: 0.80-0.88) for the single-agent baseline (DeLong test, p = 0.003). The dashed diagonal line represents chance performance.
Skill Interactions
| When | Call | Purpose |
|---|---|---|
| Need statistical values for plot | /analyze-stats |
Get computed values (AUC, CI, p-values) |
| Flow diagram for manuscript | /write-paper Phase 2 |
Coordinate with Tables & Figures plan |
| Caption review | /write-paper Phase 7 |
Final polish pass |
Error Handling
- If data is insufficient for the requested figure type, explain what is needed and ask the user.
- If a figure exceeds journal dimension limits, resize and report the adjustment.
- If text overlaps in the figure, try
tight_layout(), reduce font size, or adjust spacing. - Never fabricate data points. If sample data is needed for a template demo, explicitly label it as "example data."
CLI Tools Available
ImageMagick, Ghostscript, FFmpeg are installed and can be used for post-processing:
# Figure DPI/format conversion for journal submission
magick input.png -density 300 -units PixelsPerInch output.tiff
magick input.png -resize 1200x -quality 95 output.jpg
# CMYK conversion (some print journals require this)
magick input.png -colorspace CMYK output.tiff
Portal-ready TIFF (SNAPP .png-not-accepted / 25 MB cap)
A raw magick ... output.tiff keeps the alpha channel (transparent regions print black
on many production pipelines) and stays uncompressed (a 600-dpi RGBA TIFF blows past a
portal's 25 MB cap). export_portal_tiff.py does the flatten-and-compress a human otherwise
does by hand and verifies the result is pixel-identical to that white-flatten before
handing it over — use it when a portal accepts only .tiff/.jpeg/.eps (Springer Nature
SNAPP) or caps figure size (JACC: Asia):
python3 scripts/export_portal_tiff.py --in figure.png --out figure.tiff --max-mb 25
# LZW-compressed, RGBA→RGB white-flattened, pixel-identity-verified; exit 1 if still over the cap
# Multi-panel figure assembly (A/B/C/D panels)
magick montage panelA.png panelB.png panelC.png panelD.png \
-tile 2x2 -geometry +10+10 -density 300 combined.png
# Animated figure (GIF from frame sequence)
ffmpeg -framerate 2 -i frame_%03d.png -vf "scale=800:-1" output.gif
# Video from figure sequence (for supplementary materials)
ffmpeg -framerate 1 -i slide_%03d.png -c:v libx264 -pix_fmt yuv420p supplementary_video.mp4
AI Image Generation (Optional)
AI illustration is a supplementary option, not a requirement. Visual abstracts and figures can be completed without any API key using study figures and free illustration libraries.
If GEMINI_API_KEY is set, the generate_image.py script can generate illustrations:
python ${CLAUDE_SKILL_DIR}/scripts/generate_image.py \
"Clean medical illustration of a CT-guided lung biopsy procedure, \
flat vector style, white background, no text" \
--output output.png --aspect 16:9
Use for: procedural schematics, anatomical illustrations, pipeline diagrams. Always review AI output against the AI-Generated Figure Warning section above.
If GEMINI_API_KEY is not set, guide the user to free illustration resources:
see ${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md.
Language
- Code and figure text: English
- Communication with user: Match user's preferred language
- Medical terms: English only
Anti-Hallucination
- Never fabricate references. All citations must be verified via
/search-litwith confirmed DOI or PMID. Mark unverified references as[UNVERIFIED - NEEDS MANUAL CHECK]. - Never invent clinical definitions, diagnostic criteria, or guideline recommendations. If uncertain, flag with
[VERIFY]and ask the user.
Global-rule references
Some passages in this skill cite a path of the form ~/.claude/rules/<name>.md. Those are the
maintainer's personal global rules, kept outside this repository. They are not shipped with
this skill and will not exist on your machine; they appear only as provenance for where a
convention came from. If one of them looks like it is standing in for an instruction you actually
need, that is a bug — please open an issue, because the instruction belongs here.
Files (medsci-skills)
-
references
-
critic_rubrics
-
data_plot.md 7.5 KB
# Critic Rubric — Data Plots Apply this rubric when the generated figure is a data visualization (not a flow diagram): ROC, forest, KM, calibration, Bland-Altman, confusion matrix, Manhattan/volcano, box/violin, bar, heatmap. The Claude session should Read the rendered PNG plus any available exemplars from `references/exemplar_diagrams/{type}/`, then mark each item below as **PASS / PARTIAL / FAIL** with a one-line justification. After scoring, produce a list of concrete matplotlib/seaborn edits that would resolve every FAIL or PARTIAL item. Return the scored rubric + edit list to the user. --- ## A. Axes, labels, units 1. **X-axis label** present with units where applicable. 2. **Y-axis label** present with units where applicable. 3. **Axis tick labels** readable (no overlap, sensible density, no scientific notation where plain numbers fit). 4. **Axis limits** sensible — no wasted whitespace (e.g., ROC axes should be 0–1 exactly, forest plot x-axis should frame all CIs without clipping). 5. **Tick direction inward**, tick marks minor/major where needed. No chart-spine clutter. ## B. Legend / caption 6. **Legend present** when ≥2 series are shown; placed where it does not obscure data. 7. **Legend labels** are descriptive (not "Series 1"); include N or group size where relevant. 8. **Legend ordering** matches visual order of series (top to bottom). 9. **Caption-ready** — plot does not rely on external notes the caption cannot repeat concisely. ## C. Figure-type–specific requirements ### ROC curve - AUC and 95% CI displayed on plot or in legend - Diagonal reference line (chance) shown - Sensitivity on Y-axis (0–1), 1−Specificity on X-axis (0–1) - Multiple models distinguished by color AND line style (grayscale-safe) ### Forest plot - Effect estimates aligned to a vertical reference line (null effect) - CIs drawn as horizontal bars with appropriate caps - Box size proportional to study weight - Diamond summary at the bottom; pooled estimate and CI printed - Left column: study labels; right column: effect (95% CI) numeric - Heterogeneity statistics (I², τ², Q p-value) reported in caption or plot ### KM curve - Number at risk table below the plot - Median survival and its CI shown or reported in caption - Log-rank p-value or Cox HR reported - Curves distinguishable in grayscale (line style + color) - Censoring marks visible ### Calibration plot - Ideal diagonal line (y=x) shown - Binned observed vs. predicted points - Hosmer-Lemeshow p-value or Brier score reported - Histogram of predicted probabilities overlaid or beside the plot ### Bland-Altman - Mean difference line - ±1.96 SD limits of agreement - LoA values printed - Scatter points not clipped at plot edges ### Confusion matrix - Cells annotated with counts AND percentages - Axis labels "Predicted" / "Actual" (or "Reference") - Diagonal emphasized (darker color or heavier stroke) if useful ### Manhattan / volcano (agnostic many-exposure scan: ExWAS / EWAS / MWAS) - Significance threshold line drawn AND its basis stated (FWER/Bonferroni or FDR), with the **number of tests** in the caption - Y-axis is −log10(p) (Manhattan) or −log10(p) vs effect size (volcano) — a volcano must show effect size, not significance alone - Hits labeled sparingly (top/threshold-crossing only); the full tested set is in a supplement, not crowded onto the plot - Direction of effect distinguishable (volcano: up/down; Manhattan: a sign/colour track if signed) - Caption states whether hits are **replicated** (discovery-only scans are exploratory) ## D. Typography and accessibility 10. **Font size ≥ 8pt at final dimensions** (most journals require 6–7pt min for axis numerics, 8pt for labels). At 300 DPI in a 3.5×3.5 inch figure, this corresponds to roughly ≥25 px — use `critic_figure.py` OCR min-height flag as a proxy. 11. **Single font family** throughout (typically sans-serif). No mixed Arial/Helvetica/Times. 12. **Colors from Wong palette** or equivalent colorblind-safe scheme. `critic_figure.py` flags out-of-palette fractions >15%. 13. **Grayscale-safe** — every series is distinguishable in grayscale conversion, either through line style, marker shape, or luminosity differences. ## E. Publication readiness 14. **Vector PDF produced** in addition to PNG. 15. **Dimensions match journal spec** — width per figure_specs.md for the target journal (single column ≈3.5 in, double ≈7.0 in). 16. **DPI ≥ 300 (halftone) or ≥ 600 (line art)**. 17. **No duplicate data encoding** (e.g., color + shape + size all mapped to the same variable adds clutter without information). 18. **Statistical annotations** — where significance markers are used (`*`, `**`, `***`), the caption defines thresholds. Prefer actual p-values for publication. ## F. Exemplar comparison (if exemplars exist for this type) For non-flow types, the worked anatomy models live in `../exemplar_plots/` (e.g., `forest_plot.md`) — read the matching one and confirm the draft has every element it lists. 19. **Visual density** comparable to exemplars — not significantly sparser or more cluttered. 20. **Annotation style** — placement of summary statistics, N labels, p-values consistent with exemplars. ## G. Medical AI / prediction-model checks (added v1.1.0) Apply when the figure supports a prediction-model or medical-AI claim (TRIPOD+AI, CLAIM 2024, STARD-AI, CONSORT-AI). Source: `reporting_guideline_figure_map.md` "AI-specific figures most often missing." 21. **Calibration plot accompanies discrimination** — when the manuscript reports AUC/c-statistic, a calibration plot is also presented (or the figure is paired with one). TRIPOD+AI mandates calibration; AUC alone is insufficient evidence of model fitness. 22. **Subgroup / fairness panel** — when the deployment claim covers multiple demographic groups, sites, or scanner vendors, performance is shown stratified by at least one such axis. CLAIM 2024 §C and TRIPOD+AI both require this. 23. **Colorblind-safe + redundant encoding** — color carrying diagnostic meaning is paired with at least one non-color cue (line style, marker shape, or direct label) so the figure survives deuteranopia simulation and grayscale conversion. (Crameri 2024.) Stronger than the existing item D.13. 24. **Dataset-flow visible** — for AI papers reporting performance on a test set, the manuscript also includes a dataset-flow diagram with counts at training / tuning / internal-test / external-test splits. Required by STARD-AI, CLAIM 2024, TRIPOD+AI. If the figure under review is not the dataset-flow itself, confirm one exists elsewhere in the manuscript. 25. **Decision-curve analysis (when claiming clinical utility)** — papers that argue "this model would change clinical management" must accompany discrimination/calibration with a decision-curve plot (Vickers & Elkin, *Med Decis Making* 2006). Recommended by TRIPOD+AI. --- ## Scoring output format ``` ## Critic report (data plot, round T) | Item | Score | Note | |------|-------|------| | A.1 X-axis label | PASS | — | | A.2 Y-axis label | FAIL | Missing units ("Sensitivity" should be unitless OK; but "Time" needs "(months)") | | ... ### Required edits before next render 1. Add unit "(months)" to Y-axis label. 2. Increase legend font from 6pt to 8pt. 3. ... ### Overall verdict [ ] PASS — ready for manuscript [ ] REFINE — items above must be fixed before next round ``` Record `critic_pass: yes | partial | no` and `refine_rounds: N` in the `_figure_manifest.md` for this figure after the final round. -
flow_diagram.md 7.5 KB
# Critic Rubric — Flow Diagrams Apply this rubric when the generated figure is a flow diagram: STARD, CONSORT, PRISMA, or pipeline/methods. The Claude session should Read the rendered PNG plus any available exemplars from `references/exemplar_diagrams/{stard|consort|prisma|pipeline}/`, then mark each item below as **PASS / PARTIAL / FAIL** with a one-line justification. After scoring, produce a concrete list of source edits (D2 code changes, node label fixes, count corrections) that would resolve every FAIL or PARTIAL item. Return the scored rubric + edit list to the user. --- ## A. Structural integrity 1. **Box overlap** — No two boxes or text labels overlap. Arrows do not cross through node text. 2. **Readable edges** — Every edge has a clear source and target; direction is unambiguous; no dangling arrows. 3. **Hierarchy preserved** — Vertical or horizontal flow reads in a single dominant direction; upstream nodes appear before downstream ones. 4. **Alignment** — Peer nodes at the same logical level are aligned (baseline or center). 5. **Whitespace** — No excessive vertical gap (>2× box height) between sequential steps. No cramped collisions at decision points. ## B. Required elements (per figure type) ### STARD flow (diagnostic accuracy) - Enrollment count (consecutive vs. random, eligible n) - Included n (with inclusion criteria applied) - Excluded n with itemized reasons - Index test performed (n analyzed) - Reference standard performed (n analyzed) - Final analysis cohort (n with both tests) ### CONSORT flow (RCT) - **Enrollment** section: Assessed for eligibility (n), Excluded (n with reasons: did not meet criteria, declined, other) - **Allocation** section: Randomized (n), Allocated to each arm (n received / n did not receive intervention) - **Follow-up** section: Lost to follow-up (n, reasons), Discontinued (n, reasons) per arm - **Analysis** section: Analyzed (n), Excluded from analysis (n, reasons) per arm ### PRISMA 2020 flow (systematic review) - Records identified from databases (n per database) - Records identified from other sources (registers, citation search, etc.) - Records after duplicates removed (n) - Records screened (n) → Records excluded (n) - Reports sought for retrieval (n) → Reports not retrieved (n) - Reports assessed for eligibility (n) → Reports excluded with reasons (n, reason categories) - Studies included in review (n) / Reports of included studies (n) ### Pipeline / methods - Input data block clearly named (modality, cohort) - Preprocessing steps in sequence (resampling, normalization, augmentation) - Model / algorithm block with name or architecture - Output / task block (segmentation, classification, regression) - Evaluation metric block where applicable ## C. Numerical consistency 6. **Arithmetic balance** — At each branching node, `in = out + excluded`. Verify every subtraction explicitly. Flag any mismatch. 7. **Column totals consistent** — For parallel arms (CONSORT), arm totals sum to the randomized total at every stage. 8. **No duplicate counts** — A participant should not appear in two leaf nodes unless the diagram explicitly models this. 9. **Manuscript consistency** — Numbers in the diagram match the corresponding text in Methods/Results (apply the [VERIFY-CSV] rule — every count should trace to a CSV cell or source query). ## D. Typography and accessibility 10. **Font size ≥ 18pt** at the compact-recipe render dimensions (main boxes ≥ 20pt, exclusion boxes ≥ 18pt, italic notes ≥ 17pt). Check against the `critic_figure.py` OCR min-height flag. 11. **No truncated text** — No box shows clipped text ("Assessed for eli..."). OCR coverage check (source_word_coverage ≥ 0.95). 12. **Consistent casing** — Sentence case everywhere, or Title Case everywhere — not mixed. 13. **Wong palette or neutral only** — Fill colors drawn from the Wong colorblind-safe set plus whites/light grays. No red-green only distinctions. 14. **Grayscale-safe** — Conversion to grayscale preserves all distinctions between box categories (intent vs. exclusion vs. outcome). ## E. Publication readiness 15. **Vector format available** — Both PNG (for DOCX embedding) and PDF (for journal submission) were produced. If PDF missing, flag. 16. **Dimensions match journal spec** — Width matches the target journal's single-column or double-column specification (±0.3 in). 17. **DPI meets spec** — ≥600 DPI for line-art submissions, ≥300 DPI for halftone. 18. **Policy compliance** — Tool is D2 or a permitted auto-layout engine, not matplotlib FancyBboxPatch and not Mermaid. (Mermaid is forbidden in papers per the project's figure-toolchain policy.) ## F. Exemplar comparison (if exemplars exist for this type) 19. **Hierarchy depth** matches one of the exemplars within ±1 level. 20. **Typographic weight** — Main step labels are visually heavier (bold/larger) than parenthetical / exclusion labels, as in the exemplars. 21. **Emphasis placement** — Key cohort counts (final analysis n, primary outcome n) are visually emphasized (thicker stroke, larger font, or fill distinction), consistent with exemplar conventions. ## G. Communication-first checks (added v1.1.0) These checks operationalize `references/design_principles.md` (Nature Hum Behav 2026) and `references/flow_diagram_lessons.md`. Apply when the diagram will be circulated to senior co-authors or submitted to a peer- reviewed venue. 22. **Cognitive load** — Each column has ≤7 boxes; each diagram uses ≤3 distinct shapes (e.g., rectangle / rounded rectangle / note) and ≤3 fill colors. If the count is exceeded, fold detail into supplementary or split into a multi-panel figure. 23. **Key-message visibility** — The analytic cohort (final n included in primary analysis) is visually emphasized via thicker stroke (`penwidth ≥ 1.8`), distinct fill, or larger font, so the reader's eye lands on it within 2 seconds. 24. **Official-template fidelity** (when applicable) — Layout matches the canonical PRISMA 2020 / CONSORT 2010 / STARD 2015 / STROBE template used by the corresponding statement group. Custom layouts are acceptable for exploratory drafts but must be replaced before circulation. (See `flow_diagram_lessons.md` Lesson 1.) 25. **Exclusion-box geometry** — Exclusion side-boxes are rectangles (not `shape: note` / "dog-ear" style) when the diagram aims to match PRISMA / CONSORT canonical look. Bullets within exclusion boxes are left-aligned (Graphviz `\l`, not `\n`). 26. **Frozen-version sync** — The figure file path includes the manuscript version (`figures/v{N}/figure_1.pdf`) and the value of `v{N}` matches the current manuscript version. Edits after circulation must branch to `v{N+1}/`, never overwrite `v{N}/`. (See `flow_diagram_lessons.md` Lesson 5.) --- ## Scoring output format ``` ## Critic report (flow diagram, round T) | Item | Score | Note | |------|-------|------| | A.1 Box overlap | PASS | — | | A.2 Readable edges | PASS | — | | ... | C.6 Arithmetic balance | FAIL | Enrollment 500 ≠ Included 420 + Excluded 85 (off by 5) | | ... ### Required edits before next render 1. Correct Excluded count in node X from 85 to 80. 2. Increase font-size of "Analysis" subsection header from 18 to 22. 3. ... ### Overall verdict [ ] PASS — ready for manuscript [ ] REFINE — items above must be fixed before next round ``` Record `critic_pass: yes | partial | no` and `refine_rounds: N` in the `_figure_manifest.md` for this figure after the final round.
-
-
exemplar_diagrams
-
consort
-
README.md 385 B
# consort Exemplars This directory will hold 3–5 hand-curated consort flow/figure exemplars. Use the extraction script to add one: ```bash python skills/make-figures/scripts/extract_exemplar_from_pdf.py \ --pdf "/path/to/paper.pdf" \ --page N \ --type consort \ --label LastnameYEAR_CONSORT \ --doi 10.xxxx/yyyy ``` See `../README.md` for curation guidelines. -
template_input.yaml 1.4 KB
# CONSORT parallel-arm RCT flow diagram template. # Render with: # Rscript ../../scripts/generate_flow_diagram.R \ # --type consort --config template_input.yaml --out template_output rankdir: TB nodes: - id: assessed label: "Assessed for eligibility\n(n = 500)" - id: excl shape: note fontsize: 10 label: "Excluded (n = 100):\n• Not meeting inclusion criteria: 60\n• Declined to participate: 25\n• Other reasons: 15" rank_same_with: assessed - id: rand label: "Randomized\n(n = 400)" highlight: true - id: alloc_tx label: "Allocated to intervention (n = 200)\n• Received intervention (n = 195)\n• Did not receive intervention (n = 5)" - id: alloc_ctrl label: "Allocated to control (n = 200)\n• Received control (n = 198)\n• Did not receive control (n = 2)" - id: fu_tx label: "Lost to follow-up (n = 8)\nDiscontinued intervention (n = 4)" - id: fu_ctrl label: "Lost to follow-up (n = 6)\nDiscontinued control (n = 3)" - id: ana_tx label: "Analyzed (n = 200)\n• Excluded from analysis (n = 0)" - id: ana_ctrl label: "Analyzed (n = 200)\n• Excluded from analysis (n = 0)" edges: - {from: assessed, to: rand} - {from: rand, to: alloc_tx} - {from: rand, to: alloc_ctrl} - {from: alloc_tx, to: fu_tx} - {from: alloc_ctrl, to: fu_ctrl} - {from: fu_tx, to: ana_tx} - {from: fu_ctrl, to: ana_ctrl} - {from: assessed, to: excl, style: dashed, arrow: false, constraint: false} -
template_output.pdf 37.4 KB · in bundle
-
template_output.png 121.1 KB · in bundle
-
template_output_600.png 118.7 KB · in bundle
-
-
other
-
other_02_why.md 1.6 KB
# Why this exemplar is good — other_02 (bubble chart landscape) Hierarchy / structure: A landscape scatter/bubble chart with time on the X-axis (pre-2022 → 2025) and MMLU score on the Y-axis. A dashed logistic-like trajectory connects the frontier models across years, implicitly communicating the field-wide progression without a separate annotation layer. Whitespace & balance: The pre-2022 region is nearly empty by design — the sparseness is informative, not wasted. High-performance cluster in 2024–2025 is where bubbles densely overlap, and the axis range (0–100 MMLU) leaves enough headroom above the top models to signal the 90%+ ceiling. Typography (font size, weight, alignment): Model names are placed adjacent to their bubbles with consistent font size; no leader lines are needed because the labels sit directly on or beside each dot. The reference lines ("85% = human expert", "70% = ideal") are low-contrast gray so they guide without competing. Emphasis (which elements are visually strongest, why): Bubble size encodes release prominence, and color encodes the parent organization — two independent visual channels that give the reader a comparative read without returning to the legend. Color usage: Eight distinct organizational colors placed against a white field. The palette is high-saturation but the bubbles are translucent, which lets overlapping dots remain legible. Weaknesses (if any — nothing is perfect): The legend (top) is cramped and would benefit from more horizontal space. A few labels ("DeepSeek", "Claude 3.5 Sonnet") clip against neighbors in the 2024 cluster. No explicit units or source note in the rendered crop.
-
-
pipeline
-
pipeline_01_why.md 1.5 KB
# Why this exemplar is good — pipeline_01 (pipeline) Hierarchy / structure: A clean left-to-right pipeline — paired inputs (CXR image on top, clinical text on bottom) merge into a single downstream processor (MLLM) and terminate in a single output box. The two parallel encoder boxes are aligned horizontally, which communicates multimodal symmetry at a glance. Whitespace & balance: Generous horizontal whitespace around the central MLLM chip icon makes it the visual anchor. Input column and output column carry roughly equal ink weight, avoiding a lopsided read. Typography (font size, weight, alignment): Box labels (Image encoder / Text encoder) are bolded sans-serif; patient demographics use a smaller mono-like face to read as tabular data rather than prose. Illustrative "clinical question" is italicized and color-accented to flag that it is an example, not a label. Emphasis (which elements are visually strongest, why): Pink-tinted inputs and green output speech bubble draw attention to the clinically meaningful endpoints (question in → answer out), while the technical processing blocks sit in neutral gray. Color usage: Two saturated tints (pink for input, green for output) plus neutral gray for machinery. Colorblind-safe if converted — the saturation contrast survives grayscale. Weaknesses (if any — nothing is perfect): The MLLM "chip" icon is decorative rather than structural; could confuse readers expecting an architectural diagram. CXR thumbnail is small relative to the clinical-info panel, slightly under-weighting the image modality. -
pipeline_03_why.md 1.7 KB
# Why this exemplar is good — pipeline_03 (pipeline) Hierarchy / structure: Two-panel layout (A and B) separated by a thin vertical rule, with each panel self-contained. A shows contrastive pre-training: text encoder (top) and image encoder (bottom) converge on a similarity matrix. B shows a VQA inference pipeline flowing left → right. Panel labels sit in the top-left corner of each panel at a consistent position. Whitespace & balance: Margins around the similarity matrix give it room to read as a data object rather than decoration. The two panels carry roughly equal visual mass so neither dominates. Typography (font size, weight, alignment): Box labels use uniform sans-serif at one size; the similarity matrix cells use a mono face so Tᵢ·Iⱼ products align as a grid. The CXR/report thumbnails carry subscripts (T₁, T₂, I₁, I₂) in the same font as the matrix cells — visually tying the inputs to the matrix rows/columns. Emphasis (which elements are visually strongest, why): The diagonal of the similarity matrix is shaded darker, pulling the eye to the positive pairs — this is the pedagogical point of contrastive learning and the figure lets design carry the explanation. Color usage: Pink for text-side components, blue for image-side components. This two-channel color coding is consistent across both panels and makes the multimodal symmetry self-evident. Weaknesses (if any — nothing is perfect): Panel B's projection matrix W and token boxes are compressed; readers unfamiliar with contrastive-language-image architecture may need to read the caption closely. The "language instruction" box on the far right of B is visually detached from the main flow. -
pipeline_04_why.md 1.6 KB
# Why this exemplar is good — pipeline_04 (pipeline) Hierarchy / structure: Top row is the macro view (modality inputs → encoder → connector → LLM → generator → outputs), bottom row zooms into the "connector" block with four named variants (A MLP, B Q-Former, C MH-Attn, D Expert Captioning). A light-blue downward arrow visually tethers the macro block to its expansion — the classic "zoom-in detail" device applied cleanly. Whitespace & balance: Even horizontal spacing between connector variants. The expanded lower panel is framed in a subtle tinted rectangle that contains it without drawing attention. Typography (font size, weight, alignment): Consistent sans-serif labels throughout. Capital letters A/B/C/D tag the four variants in one size; component labels (MLP, Q-Former, MH-Attn) use the same type family as the macro row for continuity. Emphasis (which elements are visually strongest, why): The central LLM box is the largest and darkest block — correctly, because this is the figure's conceptual center. The connector variants underneath share equal weight, communicating their parallel status as alternatives rather than a sequence. Color usage: Restrained three-tone palette (blue for neural modules, orange for learnable queries/tokens, gray for data I/O). Dashed outlines mark optional/repeated elements. Colorblind-safe. Weaknesses (if any — nothing is perfect): Many modality icons (image/audio/video) repeat at both input and output; a single output cluster would reduce clutter. The Q/K/V labels inside panel C are small relative to neighboring elements. -
pipeline_05_why.md 1.7 KB
# Why this exemplar is good — pipeline_05 (pipeline / tasks illustration) Hierarchy / structure: Two stacked panels (A and B), each a four-column table-like layout. A shows report generation for two cases (pelvic radiograph and abdominal CT) side-by-side: input → reference report → generated report. B shows VQA with closed-ended vs. open-ended question variants for each case. The uniform column structure makes the cross-row comparison immediate. Whitespace & balance: Narrow column gutters (not wasteful) but generous vertical spacing between cases. Each row reads as a discrete case study. Typography (font size, weight, alignment): The strongest typographic choice is the use of colored highlights on specific phrases in the generated reports — green for confirmed findings, red for hallucinated content, orange for partial matches. This converts what would be plain prose into an annotated diff without extra legend burden, provided the reader notices the color key. Emphasis (which elements are visually strongest, why): The color-annotated report text is the primary didactic payload and is the strongest element, correctly. The input images serve as context and are sized accordingly smaller. Color usage: Color communicates semantic agreement/disagreement in text — a rare and effective use. Panel A uses gray scaffolding (headers, borders) to keep attention on the colored content. Weaknesses (if any — nothing is perfect): Red and green are the primary discriminators, which is not colorblind-safe. A shape or weight encoding (bold for hallucinations) would make it more accessible. Font size of the report text is small; readability at small figure widths could suffer. -
pipeline_06_why.md 1.6 KB
# Why this exemplar is good — pipeline_06 (pipeline / M3D architecture) Hierarchy / structure: Strict top-to-bottom flow. Volumetric CT input (single modality box with three anatomical sub-panels) feeds a 3D image encoder; parallel text-side question templates feed the text tokenizer. Both streams converge on a single LLM-with-LoRA block, then split into two downstream heads (report generation, VQA). The shape (funnel in → funnel out) mirrors the dual-task architecture. Whitespace & balance: Equal space allocated to image-side and text-side inputs; the LLM block spans the full width to signal it is the shared backbone. Tight but not cramped. Typography (font size, weight, alignment): Bolded section headers (Report Generation / VQA) for the prompt boxes; prompt contents in a slightly smaller, clearly italic-or-lighter weight. The special tokens (<|eot_id|>, <|start_header_id|>) use a monospace face — the right choice for communicating that they are literal code strings. Emphasis (which elements are visually strongest, why): The fire/snow icons (trainable/frozen) are placed on individual components, making the training-recipe claim visually inspectable. This is a small but high-value design choice — readers can audit the method by looking at the figure. Color usage: Cool blues for vision modules, warm pinks for language/prompt modules. Two-tone palette scales well to grayscale (luminance differs). Weaknesses (if any — nothing is perfect): The "trainable/frozen" legend sits in the upper right and may be missed by readers who scan left-to-right top-down. The three CT sub-views (chest/abdomen/pelvis) are small enough that anatomy is only suggested. -
pipeline_07_why.md 1.6 KB
# Why this exemplar is good — pipeline_07 (pipeline / MAIRA-Seg) Hierarchy / structure: A horizontal pipeline with input row (CXRs, pseudo-masks, task instruction, textual context) on top, processing row (image encoder, segmentation model, tokenizer/embedding) in the middle, and convergence on a single LLM block at the bottom. The vertical progression communicates "modalities in → unified tokens → one model out" without ambiguity. Whitespace & balance: The LLM block spans almost full width as a visual foundation. Upper row inputs are grouped by modality with clear gaps, avoiding the "everything dumps in" look. Typography (font size, weight, alignment): Small but uniform sans-serif labels; auxiliary labels ("Only in multi", "Pseudo-labels") are italic to mark them as conditional annotations without adding a legend. Emphasis (which elements are visually strongest, why): The LLM base block is the largest uniform colored rectangle — visually grounding the whole system as one model. Fire icons on trainable components again let the figure double as a training-recipe specification. Color usage: Warm pinks for segmentation/prompt-side modules; cool blues for vision/LLM modules. Dashed borders denote optional paths ("Only in multi"). Consistent with the color code used in other architecture figures in the paper. Weaknesses (if any — nothing is perfect): The dense upper input row competes for attention; a subtle grouping box or lane separator would help. The segmentation-tokens-extractor sits off the main flow axis, which introduces a jog in the layout that briefly interrupts the top-down read. -
pipeline_08_why.md 1.7 KB
# Why this exemplar is good — pipeline_08 (pipeline / CLIMB foundation model) Hierarchy / structure: Three-stage landscape layout separated by vertical rules: Training (left) | Specialized Encoder (center) | Testing + Downstream (right). Inside the Training panel, three modality lanes (Graphs, 1D signals, 2D&3D images) each carry three exemplar thumbnails, creating a grid-of-grids that communicates breadth without listing every dataset. Whitespace & balance: Equal vertical space per modality lane; the encoder column is visually narrow to read as a bottleneck/convergence point. Right-side testing block has enough margin around the OOD dataset callout to keep it distinct. Typography (font size, weight, alignment): Modality headers (Graphs, 1D signals, 2D&3D images) are larger and bolder than thumbnail captions. Section headers (Training / Testing) sit above their column in matching weight. Emphasis (which elements are visually strongest, why): The thumbnail grid at training time is the strongest visual payload — it answers "what does multimodal breadth look like?" in one glance. The encoder column is intentionally understated because it is a conceptual stage, not a specific architecture. Color usage: Subtle pastel backgrounds separate stages (soft pink for training, green for testing, peach for downstream) without compromising the thumbnails' own color. Downstream task illustrations (physician + patient) add warmth without clutter. Weaknesses (if any — nothing is perfect): Ten or more thumbnail captions at small sizes risk clipping on reprints. The "transfer" and "finetuned" arrows in the testing column could be slightly thicker to read as a pipeline rather than decoration. -
pipeline_09_why.md 1.7 KB
# Why this exemplar is good — pipeline_09 (evaluation-metrics comparison) Hierarchy / structure: A 3×2 grid where all six evaluation methods share identical sub-layout — method title (bar header), candidate excerpt (middle), and two-bullet summary (O = what is captured, △ = what is missed). One shared reference sentence sits above the grid, establishing the common ground-truth all six metrics are being compared against. Whitespace & balance: The top reference strip spans full width and is visually distinct from the grid below. Within each cell, internal padding is consistent and there is clear separation between the colored header bar and the body — giving each metric equal visual real-estate. Typography (font size, weight, alignment): Bold uppercase metric names in colored header bars; candidate text below in regular weight with discriminating phrases colored inline (e.g., "right-sided" in red for a miss). The O/△ legend uses symbols instead of words, keeping each cell compact. Emphasis (which elements are visually strongest, why): The six colored header bars act as chapter tabs, making the grid scannable. Colored phrases inside the candidate text are the didactic focus — they show precisely what each metric is scoring. Color usage: Six distinct pastel headers, chosen for differentiation rather than semantic meaning. Candidate-text color (red/green/blue) is semantic. Palette is varied but low-saturation enough to avoid fatigue. Weaknesses (if any — nothing is perfect): Six colors with no ordinal relationship could be reduced; the reader doesn't need to distinguish cells by color since the grid layout already separates them. Semantic text colors (red for missed) overlap with header colors in a few cells. -
pipeline_10_why.md 1.9 KB
# Why this exemplar is good — pipeline_10 (clinical integration overview) Hierarchy / structure: Three horizontal strips. Top = clinical workflow (Patient → Triage → Consultation → Diagnosis → Treatment → Monitoring) rendered as a linear process diagram. Middle = radial diagram with a radiologist at center surrounded by MLLM support tasks. Bottom = radial diagram with an AI icon at center surrounded by clinical-integration challenges. Two radial compositions mirror each other visually — a deliberate parallel that frames "capabilities vs. challenges". Whitespace & balance: Each strip is framed in a light background panel with its title on the left (Clinical Workflow / MLLM Support for Radiologist / Key Challenges for Clinical Integration) — the row-label placement anchors the reader and allows side-by-side reading without a separate key. Typography (font size, weight, alignment): Row-label italics on the left are consistent. Radial labels (Visual Question Answer, Clinical Decision Support, etc.) use sentence case and are placed at consistent radial distances from center. Emphasis (which elements are visually strongest, why): The two central icons (radiologist, AI chip) draw the eye first, making each radial panel's subject unmistakable. The triage/diagnosis/treatment icons in the top strip are sized uniformly so no single step is over-emphasized — correctly, because this is a generic workflow. Color usage: Warm peach/cream for support, cool blue for challenges — a clean semantic contrast. Individual radial spokes are lightly tinted to differentiate themes within each group. Weaknesses (if any — nothing is perfect): The radial metaphor collapses any ordering between spokes — a reader cannot tell which challenge is most pressing or which MLLM role is primary. Some icon glyphs (e.g., "Final Decision by Radiologist") are small and may not read clearly when printed at single-column width. -
README.md 389 B
# pipeline Exemplars This directory will hold 3–5 hand-curated pipeline flow/figure exemplars. Use the extraction script to add one: ```bash python skills/make-figures/scripts/extract_exemplar_from_pdf.py \ --pdf "/path/to/paper.pdf" \ --page N \ --type pipeline \ --label LastnameYEAR_PIPELINE \ --doi 10.xxxx/yyyy ``` See `../README.md` for curation guidelines.
-
-
prisma
-
README.md 381 B
# prisma Exemplars This directory will hold 3–5 hand-curated prisma flow/figure exemplars. Use the extraction script to add one: ```bash python skills/make-figures/scripts/extract_exemplar_from_pdf.py \ --pdf "/path/to/paper.pdf" \ --page N \ --type prisma \ --label LastnameYEAR_PRISMA \ --doi 10.xxxx/yyyy ``` See `../README.md` for curation guidelines. -
template_input.yaml 1.9 KB
# PRISMA 2020 systematic review / meta-analysis flow diagram template. # Render with: # Rscript ../../scripts/generate_flow_diagram.R \ # --type prisma --config template_input.yaml --out template_output rankdir: TB nodes: - id: id_db label: "Records identified from databases\n(n = 3,500)\n• PubMed: 1,800\n• Embase: 1,200\n• Cochrane: 500" - id: id_other label: "Records identified from other sources\n(n = 45)\n• Citation search: 30\n• Registers: 15" rank_same_with: id_db - id: dedup label: "Records after duplicates removed\n(n = 2,700)" - id: screened label: "Records screened (title/abstract)\n(n = 2,700)" - id: excl_ta shape: note fontsize: 10 label: "Records excluded\n(n = 2,480)" rank_same_with: screened - id: ft_sought label: "Reports sought for retrieval\n(n = 220)" - id: ft_notret shape: note fontsize: 10 label: "Reports not retrieved\n(n = 12)" rank_same_with: ft_sought - id: ft_assessed label: "Reports assessed for eligibility\n(n = 208)" - id: ft_excl shape: note fontsize: 10 label: "Reports excluded (n = 180):\n• Wrong population: 72\n• Wrong index test: 48\n• Wrong outcome: 35\n• Abstract only: 15\n• Duplicate cohort: 10" rank_same_with: ft_assessed - id: included label: "Studies included in review\n(n = 28)\nStudies in quantitative synthesis\n(n = 24)" highlight: true edges: - {from: id_db, to: dedup} - {from: id_other, to: dedup, constraint: false} - {from: dedup, to: screened} - {from: screened, to: ft_sought} - {from: ft_sought, to: ft_assessed} - {from: ft_assessed, to: included} - {from: screened, to: excl_ta, style: dashed, arrow: false, constraint: false} - {from: ft_sought, to: ft_notret, style: dashed, arrow: false, constraint: false} - {from: ft_assessed, to: ft_excl, style: dashed, arrow: false, constraint: false} -
template_output.pdf 40.5 KB · in bundle
-
template_output.png 115.1 KB · in bundle
-
template_output_600.png 113.5 KB · in bundle
-
-
stard
-
README.md 377 B
# stard Exemplars This directory will hold 3–5 hand-curated stard flow/figure exemplars. Use the extraction script to add one: ```bash python skills/make-figures/scripts/extract_exemplar_from_pdf.py \ --pdf "/path/to/paper.pdf" \ --page N \ --type stard \ --label LastnameYEAR_STARD \ --doi 10.xxxx/yyyy ``` See `../README.md` for curation guidelines. -
template_input.yaml 1.2 KB
# STARD 2015 diagnostic-accuracy flow diagram template. # Render with: # Rscript ../../scripts/generate_flow_diagram.R \ # --type stard --config template_input.yaml --out template_output rankdir: TB nodes: - id: eligible label: "Eligible patients\n(n = 500)" - id: excl shape: note fontsize: 10 label: "Excluded (n = 50):\n• Contraindication to index test: 20\n• No reference standard available: 20\n• Declined: 10" rank_same_with: eligible - id: index label: "Received index test\n(n = 450)" highlight: true - id: ref label: "Received reference standard\n(n = 450)" - id: idx_pos label: "Index test positive\n(n = 180)" - id: idx_neg label: "Index test negative\n(n = 270)" - id: tp label: "Reference positive\nTP = 160" - id: fp label: "Reference negative\nFP = 20" - id: fn label: "Reference positive\nFN = 15" - id: tn label: "Reference negative\nTN = 255" edges: - {from: eligible, to: index} - {from: index, to: ref} - {from: ref, to: idx_pos} - {from: ref, to: idx_neg} - {from: idx_pos, to: tp} - {from: idx_pos, to: fp} - {from: idx_neg, to: fn} - {from: idx_neg, to: tn} - {from: eligible, to: excl, style: dashed, arrow: false, constraint: false} -
template_output.pdf 33.9 KB · in bundle
-
template_output.png 78.4 KB · in bundle
-
template_output_600.png 76.9 KB · in bundle
-
-
strobe
-
template_input.yaml 1.4 KB
# STROBE cohort-study flow diagram template. # Render with: # Rscript ../../scripts/generate_flow_diagram.R \ # --type strobe --config template_input.yaml --out template_output rankdir: TB nodes: - id: source label: "Source population\n(Database XYZ, 2010–2023; total records = 120,000)" - id: unique label: "Unique subjects after deduplication\nN = 85,000" - id: excl1 shape: note fontsize: 10 label: "Excluded (n = 5,000):\n• Missing age/sex: 1,200\n• Outside age range: 3,800" rank_same_with: unique - id: eligible label: "Meeting eligibility criteria\nN = 80,000" - id: excl2 shape: note fontsize: 10 label: "Excluded (n = 2,500):\n• Prior event at baseline: 1,500\n• Follow-up ≤ 0 days: 1,000" rank_same_with: eligible - id: cohort label: "Analytic cohort\nN = 77,500" highlight: true - id: exp label: "Exposed\nn = 20,000" - id: unexp label: "Unexposed\nn = 57,500" - id: out_exp label: "Incident events\n1,200 (6.0%)" - id: out_unexp label: "Incident events\n2,300 (4.0%)" edges: - {from: source, to: unique} - {from: unique, to: eligible} - {from: eligible, to: cohort} - {from: cohort, to: exp} - {from: cohort, to: unexp} - {from: exp, to: out_exp} - {from: unexp, to: out_unexp} - {from: unique, to: excl1, style: dashed, arrow: false, constraint: false} - {from: eligible, to: excl2, style: dashed, arrow: false, constraint: false} -
template_input_pptx.yaml 1.5 KB
# STROBE cohort-study flow diagram — editable PPTX schema. # # Render with: # python3 ../../scripts/build_strobe_template.py \ # --config template_input_pptx.yaml \ # --out template_output.pptx # # Output is a fully-editable PowerPoint slide (13.33 × 10 in) with: # - left-side colored phase column (one merged label per consecutive same-stage spine block) # - center white-fill spine of cohort-stage boxes # - right-side white-fill exclusion boxes, connected by arrows # # Stage box text colour auto-picks BLACK or WHITE from fill luminance. # All text frames have word_wrap = True so prose never overflows. slide_size: [13.33, 10.0] title: "Figure 1. STROBE participant flow diagram" stages: - {name: Identification, color: "#cfe1f5"} - {name: Screening, color: "#cfe1f5"} - {name: Inclusion, color: "#cfe1f5"} - {name: Analysis, color: "#cfe1f5"} spine: - id: source stage: Identification text: "Source population\n(Database XYZ, 2010–2023)\ntotal records = 120,000" - id: unique stage: Screening text: "Unique subjects after deduplication\nN = 85,000" - id: eligible stage: Inclusion text: "Meeting eligibility criteria\nN = 80,000" - id: cohort stage: Analysis text: "Analytic cohort\nN = 77,500" exclusions: - after: unique text: "Excluded (n = 5,000):\n- missing age/sex (n = 1,200)\n- outside age range (n = 3,800)" - after: eligible text: "Excluded (n = 2,500):\n- prior event at baseline (n = 1,500)\n- follow-up ≤ 0 days (n = 1,000)" -
template_output.pdf 47.2 KB · in bundle
-
template_output.png 111.1 KB · in bundle
-
template_output.pptx 28.7 KB · in bundle
-
template_output_600.png 108.6 KB · in bundle
-
-
README.md 3.8 KB
# Exemplar Diagrams — Quality Anchors for the Critic Loop Visual anchors the Critic Loop reads before judging a newly generated figure: *what does a well-composed one actually look like?* ## What is here — and what is deliberately not | | | |---|---| | **`{type}/template_output*.png`** | Diagrams **this skill renders itself** (`scripts/generate_flow_diagram.R` + the `template_input.yaml` beside them). Ours. Safe to ship. | | **`{label}_why.md`** | **The teaching content.** 50–100 words on *why* an exemplar works — hierarchy, whitespace, typography, emphasis, colour. Written by us. This is what the Critic Loop actually learns from. | | ~~`{label}.png` cropped from a published paper~~ | **Removed 2026-07-14 — see below.** | ## Why the paper figures are gone This directory held ten PNGs **cropped from published papers**. The old README said so plainly, and promised each carried a `.meta.yaml` recording *"source PDF, page, DOI, crop coords"*, and that the sidecar *"records DOI and source for every exemplar."* **It did not.** The files recorded `label`, `figure_type` and `dpi`. No source. No DOI. No licence. Eight of the eighteen images had no metadata at all. The safeguard the README described had never been implemented. The old README also argued fair use, on the grounds that the exemplars are *"not redistributed as part of generated figures"* — the Critic Loop only looks at them. That is true, and it is not the question. **They were redistributed as part of the package**: this repository is **MIT-licensed** and ships on npm and as a classroom ZIP that every user downloads. MIT tells the world it may *"use, copy, modify, merge, publish, distribute, sublicense, and sell"* what is inside. We were granting those rights over other people's figures — without knowing whose, without a licence, without credit. Some were probably open-access and freely reusable with attribution. We cannot say which, because the provenance was never recorded, and **a permission you cannot demonstrate is not a permission.** The `_why.md` notes stay. They are ours, and they are where the value was: a paragraph explaining *why* a two-tone palette survives greyscale teaches more than the picture it was written about. **A figure you may legally read is not a figure you may legally ship.** That distinction is the whole reason for this file. ## Bringing your own visual anchors The Critic Loop reads whatever exemplars it finds here. 1. Drop them into `{type}/` **on your own machine.** They stay local; nothing here is uploaded anywhere, and a local file you never commit is never redistributed. 2. Give each one a sidecar: ```yaml label: "pipeline_11" figure_type: "pipeline" source: "Author et al., Journal Name, 2025" doi: "10.1234/example" license: "CC-BY-4.0" # must be true, and must permit redistribution ``` 3. If you want to **contribute** an exemplar back to the project, the licence has to permit redistribution — CC-BY, CC0, or your own work. `scripts/check_bundled_media_license.py` enforces that in CI: an image that ships without a declared, redistributable licence fails the build. ## Layout ``` exemplar_diagrams/ ├── strobe/ # cohort / cross-sectional / case-control flow ├── stard/ # diagnostic-accuracy flow ├── consort/ # RCT participant flow ├── prisma/ # systematic-review selection flow ├── pipeline/ # methods / algorithm flow (design notes only) └── other/ # (design notes only) ``` Each type directory holds `template_input.yaml` (the config the R script consumes), `template_output*.png` (what it renders), and any `_why.md` design notes. Render one yourself: ```bash Rscript ../../scripts/generate_flow_diagram.R \ --type prisma --config prisma/template_input.yaml --out prisma/template_output ```
-
-
exemplar_plots
-
ai_assistance_effect.md 3.5 KB
# Exemplar anatomy — AI-assistance effect (reader-level paired improvement) A worked **anatomy model** for the signature figure of an **AI-assistance reader study** — the one that answers "does the AI change what each reader does, and **for whom**". Where `mrmc_roc.md` shows the reader-study comparison in ROC space, this figure shows the **within-reader change**: each reader's performance **unaided → AI-aided** on the same cases, as a paired slopegraph (or dumbbell), so the reader-level effect, its spread, and any reader who is *harmed* are all visible. It carries the flagship claim of these studies ("AI narrows the non-radiologist gap"). Pairs `mrmc_roc.md`, `analyze-stats` `table-standards/table-types/reader_study.md`, and the sizing in `calc-sample-size` (Test 14 / `references/mrmc_reader_study_sample_size.md`). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - **One line (or dumbbell) per reader**, connecting the reader's metric **unaided → aided** on the **same cases** (a within-reader, crossover comparison). State the metric — the clinically relevant one: accuracy, sensitivity/specificity at the operating point actually used, or per-reader AUC. - **Stratification by reader group** (experience: resident → attending; or specialty: radiologist vs non-radiologist) by colour or small-multiple panels — the "who benefits" axis is usually the point of the study. - The **reader-averaged change (ΔAUC / Δaccuracy) with its MRMC 95% CI** (reader **and** case variance — Obuchowski–Rockette), and the **non-inferiority margin** if that is the design. - **Readers whose performance declined with AI marked** — automation bias / over-reliance is a real effect; the figure must be able to show harm, not only benefit. - A **reading-time companion** (unaided vs aided, same paired layout) when efficiency is a stated endpoint. - Caption states the **design** (crossover + washout), that the **cases were held constant**, and the **reader sample** and its generalisation limit. ## Discipline (what the figure must not do) - **Do not show only the group mean** (a single before/after bar pair) — it hides the reader spread, who benefits, and any reader who got **worse** with AI; the per-reader detail is the finding. - **Do not use an unpaired comparison or a fixed-reader CI** — the readers read the same cases, so the uncertainty is the **paired MRMC** variance, not two independent groups. - **Do not claim "AI helps readers" from an upward mean** without the stratified effect — a mean gain can be one subgroup improving while another is unaffected or harmed. - **Do not omit the harmed readers or the washout** — an unwashed second read is confounded by case recognition, and hiding declines overstates benefit. - **Do not read a subgroup the study was not sized for** as a firm effect — reader subgroups are usually underpowered (ties to `calc-sample-size` Test 14). ## Common omission - The **per-reader lines**, the **stratification by experience/specialty**, the **harmed-reader cases**, and the **paired MRMC CI** on the averaged change — the elements this figure most often drops, and the ones that turn "AI helps on average" into the defensible, *for-whom* claim a reader study exists to make. Cross-reference `mrmc_roc.md`, `critic_rubrics/data_plot.md`, the diagnostic-accuracy probes `peer-review/references/domain-probes/diagnostic_accuracy.md`, and `analyze-stats` `table-standards/table-types/reader_study.md`. -
bland_altman.md 3.4 KB
# Exemplar anatomy — Bland–Altman agreement plot (two continuous methods) A worked **anatomy model** for a Bland–Altman figure — the agreement counterpart to a method- comparison scatter. Complements the `critic_rubrics/data_plot.md` §C *Bland–Altman* checklist (this composes; the rubric scores). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `table-standards/table-types/agreement.md` (the LoA / ICC reliability table). ## Elements - **Difference (y) vs mean of the two methods (x)** — y = (method A − method B), x = (A + B)/2. Plotting the difference against *one* method (e.g., the reference) induces a spurious slope; the mean of the two is the correct abscissa. - **Bias line** = the mean difference, drawn horizontally, with its value and **95% CI** printed. - **95% limits of agreement (LoA)** = bias ± 1.96·SD of the differences, drawn as two horizontal lines, with **a confidence interval on each limit** (the LoA are themselves estimates and are wide at small n) — show the CI as a footnote value or a shaded band around each LoA. - **The a-priori clinically acceptable difference band** overlaid, so the reader can see at a glance whether the LoA fall inside the margin that was defined *before* the analysis. - **Proportional-bias check**: a regression of the difference on the mean; if the slope is non-zero (the cloud fans out or tilts), state it and model the SD or log-transform rather than quoting a single constant LoA. - **% of points within the LoA** annotated (≈95% expected) and **scatter points not clipped** at the plot edges — outliers beyond the LoA are the most informative points and must remain visible. - **Units stated on both axes** (the difference is in measurement units, not %), and n pairs given. ## Discipline (what the figure must not do) - **It is not a correlation/regression plot** — do not report Pearson r or R² as evidence of agreement. High correlation is fully compatible with large systematic bias; correlation measures association along a line, not closeness to identity. - **Do not quote a single constant LoA when bias is proportional** — if the differences widen with magnitude (heteroscedasticity), constant ±1.96·SD limits are wrong across the range; log-transform or model the SD as a function of the mean. - **Do not omit the CI on the LoA** — at small n the limits are imprecise, and an LoA that looks inside the acceptability margin may not be once its upper CI is shown. - **Handle repeated measures correctly** — with multiple pairs per subject, the naïve SD of all differences understates variability; use a repeated-measures Bland–Altman (variance-components) method and report the number of replicates per subject. - **Clinical acceptability is a pre-specified judgement**, not read off the plot after the fact — state the margin and its source, and conclude agreement only if the LoA (with CI) fall inside it. ## Common omission - The **CI on the limits of agreement**, the **proportional-bias check**, and the **pre-defined clinical-acceptability band** — the elements Bland–Altman figures most often drop, and the ones that decide whether two methods are interchangeable rather than merely correlated. Cross-reference `critic_rubrics/data_plot.md` §C (Bland–Altman) and `analyze-stats` `table-standards/table-types/agreement.md`. -
calibration_plot.md 2.5 KB
# Exemplar anatomy — calibration plot (risk-prediction model) A worked **anatomy model** for a calibration figure — the probability counterpart to the ROC/PR discrimination plot. Complements the `critic_rubrics/data_plot.md` §C / §G calibration checks (this composes; the rubric scores). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `templates/dca_plot.R` and the TRIPOD+AI / `exemplar_plots/roc_pr.md` discrimination side. ## Elements - **Predicted probability (x) vs observed frequency (y)**, both 0–1, with the **45° ideal line**. - A **smooth/flexible calibration curve** (loess or restricted cubic spline) rather than only a handful of binned points — binning into deciles hides shape and is sensitive to the cut-points. - **Calibration slope and intercept** reported in the panel: slope < 1 signals overfitting (extreme predictions too extreme); the **intercept is calibration-in-the-large** (mean predicted vs mean observed risk), and intercept ≠ 0 signals systematic over/under-prediction. - A **distribution of predicted risks** (rug/histogram under the axis), so the reader sees where the data actually live — calibration in a region with no patients is uninformative. - **On the validation/external set**, not the development set; with a CI band on the curve. ## Discipline (what the figure must not do) - **Do not present discrimination (AUROC) as evidence of calibration** — a model can rank well yet be systematically miscalibrated; both are required when a probability drives a decision. - **Do not rely on the Hosmer–Lemeshow test alone** — it is low-powered, depends on arbitrary grouping, and a non-significant p is not evidence of good calibration; show the curve + slope/ intercept. - **Do not extrapolate** the curve into probability ranges with no observations (see the rug). - If recalibration was applied, say so and show calibration **after** it on held-out data. - Where a threshold/decision is proposed, pair calibration with a **decision-curve (net-benefit)** analysis. ## Common omission - The **slope/intercept**, the **predicted-risk distribution**, and **calibration on the external set** — the elements calibration figures most often drop, and the ones that decide whether the probabilities can be trusted at the bedside. Cross-reference `critic_rubrics/data_plot.md` §C/§G, the `peer-review/references/exemplar_reviews/calibration_missing.md` finding, and `analyze-stats` `templates/dca_plot.R`. -
clinical_timeline.md 3.7 KB
# Exemplar anatomy — clinical timeline for a case report A worked **anatomy model** for a CARE case-report timeline figure. This is a synthetic teaching model: it describes what the figure must show, not a layout to copy from a published report. Use it when a case report has more than two meaningful events or when the diagnostic reasoning depends on the order of symptoms, tests, treatment, and outcomes. ## Elements - **Relative time axis** — use `Day 0`, `Postoperative day 3`, `Week 6`, or `Month 12` rather than exact calendar dates unless exact dates are essential and approved for publication. - **Event lanes** — separate the course into readable lanes such as symptoms/examination, diagnostic tests, imaging/pathology, treatment/intervention, and outcome/follow-up. Avoid a single overloaded line with every event stacked on top of it. - **Index presentation marker** — make the initial presentation or referral point visually clear so readers can orient the pre-presentation history and post-intervention course. - **Diagnostic assessment markers** — include the decisive test modality, key finding, and diagnostic impression in a compact label. If imaging is central, pair the timeline with an annotated image panel rather than forcing all visual detail into the timeline. - **Intervention markers** — show the intervention start, changes, dose/procedure/device details only when they alter interpretation, and discontinuation or complication points. - **Follow-up endpoint** — include the final follow-up interval and outcome. A timeline that ends at discharge when the manuscript claims recovery at 3 months is incomplete. - **Uncertainty/negative findings** — include only the negative tests that changed diagnostic reasoning. Routine normal findings belong in the text or table, not the figure. - **Legend linkage** — the legend should state that all labels are de-identified and relative to the index presentation; define abbreviations and outcome scales. ## Annotated imaging panel pairing For imaging-heavy case reports, use a two-part figure when possible: - **Panel A: clinical timeline** — concise event sequence and outcome. - **Panels B-D: representative images** — modality/sequence, anatomical location, arrows or labels for the key finding, and an image-level note that all identifiers are removed. - **No decorative anatomy icons** unless they carry information. If an icon is used, it must clarify anatomical location or treatment pathway and have a license source. ## Discipline (what the figure must not do) - **Do not expose identifiers** — no dates, accession numbers, institution names, patient initials, faces, tattoos, embedded DICOM overlays, or geography-specific labels. - **Do not imply causality with arrow grammar** — use sequence arrows for chronology, but avoid visual wording that makes treatment look proven to cause the outcome unless the text supports that claim. - **Do not overpack the timeline** — if more than 10-12 events are needed, split into lanes or move secondary laboratory values to a table. - **Do not omit scale/units** — laboratory trends need units and outcome scores need the named scale. - **Do not let the image panel become decorative** — every arrow should correspond to a finding named in the case presentation or legend. ## Common omission - The **final follow-up endpoint**, **relative-time de-identification**, and **diagnostic-reasoning sequence**. These are the elements that make the figure satisfy CARE's timeline expectation rather than serving as a decorative chronology. Cross-reference `write-paper/references/exemplar_case_report.md`, `write-paper/references/paper_types/case_report.md`, and the CARE checklist in `/check-reporting`. -
confusion_matrix.md 3.3 KB
# Exemplar anatomy — confusion matrix (classifier error structure) A worked **anatomy model** for a confusion-matrix figure — the per-class error breakdown behind a single accuracy number. Complements the `critic_rubrics/data_plot.md` §C *confusion matrix* checklist (this composes; the rubric scores). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `exemplar_plots/roc_pr.md` (the threshold-free discrimination side) and `analyze-stats` `table-standards/table-types/diagnostic_accuracy.md`. ## Elements - **TP / FP / FN / TN laid out as a 2×2 grid** (binary case) with **explicit axis labels**: one axis **Predicted (model)**, the other **Actual / Reference (truth)** — never leave the reader to guess which axis is which, and state the positive class. - **Raw counts in every cell**, so totals and prevalence are recoverable; a percentage-only matrix hides the n behind each rate. - **Row-normalized (recall / sensitivity) and column-normalized (precision / PPV)** views shown alongside the counts — recall normalizes over true class (rows), precision over predicted class (columns); the two answer different questions and must not be conflated. - **The operating threshold stated** — a confusion matrix is a single point on the ROC/PR curve; give the probability cut-off used and how it was chosen (and on which data, not the test set). - **Per-class metrics derivable and reported** (sensitivity, specificity, PPV, NPV, F1) with their CIs, rather than one global accuracy. - **Diagonal emphasized** (heavier stroke or luminosity) only as a reading aid — correct predictions on the diagonal, errors off it. ## Discipline (what the figure must not do) - **Do not let a high overall accuracy stand in for performance** — under class imbalance a classifier that always predicts the majority class scores high accuracy while the minority class fails completely; the off-diagonal recall for the rare class is the load-bearing number, and a class-imbalance caveat belongs in the caption. - **Do not normalize ambiguously** — label whether percentages are over rows (recall), columns (precision), or the grand total; an unlabeled "%" cell is uninterpretable. - **Do not present the matrix as threshold-free** — it is threshold-dependent; pair it with the ROC/PR curve (`roc_pr.md`) so the reader sees the whole operating range, not one chosen point. - **Do not report the matrix on the data used to pick the threshold** — fix the threshold on derivation data and report the matrix on the held-out/test set. ## Multi-class extension - For K classes, show the **K×K matrix** with the same Predicted/Actual axes; report **per-class** recall and precision plus a **macro-average** (unweighted over classes, so rare classes are not drowned out) alongside any micro/weighted average, and name which average each headline number is. ## Common omission - The **dual row/column normalization (recall vs precision)**, the **stated operating threshold**, and the **class-imbalance caveat** — the elements confusion-matrix figures most often drop, and the ones that stop a high accuracy from hiding a failing minority class. Cross-reference `critic_rubrics/data_plot.md` §C (confusion matrix), `exemplar_plots/roc_pr.md`, and `analyze-stats` `table-standards/table-types/diagnostic_accuracy.md`. -
decision_curve.md 3.9 KB
# Exemplar anatomy — decision curve (net-benefit / DCA) A worked **anatomy model** for a decision-curve (net-benefit) figure — the clinical-utility counterpart to the ROC/PR discrimination plot and the calibration probability plot. A model can discriminate and calibrate well yet still add no *decision* value over treating everyone or no one; the decision curve is what shows whether acting on the model helps across a clinically plausible range of thresholds. Complements `critic_rubrics/data_plot.md` §C/§G (this composes; the rubric scores) and pairs with `analyze-stats` `references/templates/dca_plot.R` and the `analyze-stats` `table-standards/table-types/incremental_value.md` added-value table. Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - **Threshold probability (x) vs net benefit (y)**. The x-axis is the risk threshold at which a patient/clinician would opt for the action (treat, biopsy, admit); the y-axis is net benefit, in units of *true positives per patient*, already penalised for false positives at that threshold. - The **two reference strategies on every panel**: **treat-all** (a sloping line that crosses zero at the prevalence) and **treat-none** (the horizontal line at net benefit = 0). The model is useful only over the threshold range where its curve sits **above both** references. - The **model curve(s)** across a stated, clinically justified **threshold range** (e.g. 5–40%), not a single point — the whole point is to show utility across the plausible decision region, and to name where that region comes from. - When models are compared, **each model as its own curve** on the same axes (and, if shown, the same external data), so the reader sees over which thresholds one strategy dominates. - The **operating threshold** the paper actually proposes, annotated on the curve, with the net benefit there — tying the figure back to the deployment decision. - Built on the **validation/external** data with the model **calibrated** (see below), at a single stated horizon for time-to-event outcomes. ## Discipline (what the figure must not do) - **Do not read net benefit as accuracy** — it is true positives minus weighted false positives; a curve only marginally above treat-all means little added utility even if AUROC looks strong. - **Do not omit the treat-all and treat-none references** — a model curve alone is uninterpretable; utility is defined *relative* to acting on everyone or no one. - **Do not show an uncalibrated model** — net benefit depends on the predicted probabilities, so a miscalibrated model gives a misleading curve; pair with `calibration_plot.md` and recalibrate first if needed. - **Do not extend the threshold range past where decisions are actually made**, and do not let the curve wander into thresholds with no patients (cross-reference the predicted-risk distribution). - **Do not quote a single "the model is better" threshold** without showing the range; report the interval over which the model dominates and name the proposed operating threshold. - For time-to-event outcomes, **state the horizon** and use a censoring-aware net benefit; a naïve complete-case DCA over a censored cohort is biased. ## Common omission - The **treat-all/treat-none references**, the **justified threshold range** (where the probabilities come from clinically), and **calibration before the curve** — the elements decision curves most often drop, and the ones that decide whether the figure supports a real *use-the-model* claim rather than a discrimination claim. Cross-reference `critic_rubrics/data_plot.md` §C/§G, the `peer-review/references/exemplar_reviews/calibration_missing.md` finding, `analyze-stats` `references/templates/dca_plot.R`, and the added-value table standard `analyze-stats` `references/table-standards/table-types/incremental_value.md`. -
external_validation_comparison.md 3.1 KB
# Exemplar anatomy — external-validation performance across cohorts A worked **anatomy model** for the figure that carries the **#1 acceptance lever** of a clinical DL validation study: performance **across cohorts** — internal vs one or more external sets, or by **site / scanner / vendor / sequence** — shown with its **honest drop**. It is the visual proof of generalization, and the figure a reviewer scans first. Pairs `design-study/references/venue_accept_recipe.md` (external validation = top lever), `combine_models_ablation_design.md`, and `analyze-stats` `table-standards/table-types/model_comparison.md`. Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - The **metric by cohort** (Dice / AUC / HD95) as a **forest or dumbbell** (one row per cohort), with **internal as the labelled reference** and each external cohort below it — so the **drop** is read at a glance, not buried in a table. - **Per-cohort N and a CI on each estimate** — for Dice, a **bootstrap (BCa) CI on per-case values** (a t-interval is closed-form but the metric is bounded and bunches near the ceiling, so resample whole patients instead); the CI width makes a small external cohort's uncertainty honest. - The **Δ from internal** annotated per external cohort (the generalization gap), and, if the design is non-inferiority, the **margin**. - **Faceting by structure / subgroup / sequence** where the endpoint demands it, so a per-organ or per-sequence weakness is visible rather than averaged away. - **The failure mode marked** — the cohort / structure / sequence where the model broke (e.g., a contrast-sequence collapse, a small-structure floor) shown, not dropped. ## Discipline (what the figure must not do) - **Do not show only the internal result** — a validation figure without the external axis is not a validation figure. - **Do not plot an external number ≈ internal without its N and CI** — a suspiciously **flat** external result is the visual signature of leakage / tuning-on-test; the CI and N are what make a flat result credible or expose it. - **Do not pool cohorts into one bar** — report **per-cohort**; pooling hides which site/vendor drove the drop. - **Do not omit the failed subgroups / structures / sequences** — hiding the decline overstates generalization and invites the reviewer's first probe (an honest negative is an acceptance asset). - **Do not use a naïve normal CI for Dice** — bounded, skewed per-case scores need a bootstrap CI. ## Common omission - The **per-cohort N + CI**, the **Δ-from-internal** annotation, and the **failed subgroups** — the elements this figure most often drops, and the ones that turn "it generalizes" into a claim a reviewer can check. The case count behind a precise external estimate is a design-time decision (`calc-sample-size` `references/segmentation_metric_sample_size.md`). Cross-reference `forest_plot.md` (layout kin), `critic_rubrics/data_plot.md`, and `/model-validation` (internal vs external, tuning-on-test). -
forest_plot.md 3.1 KB
# Exemplar anatomy — meta-analysis forest plot A worked **anatomy model** for a publication-grade pairwise meta-analysis forest plot, complementing the `critic_rubrics/data_plot.md` §C *forest* checklist (this shows the full composition; the rubric scores a draft against it). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `templates/forest_plot.py` / `meta_analysis.R`. ## Elements (top to bottom) - **Per-study row**: study label (author year), the effect estimate as a **square sized by weight**, and its **95% CI** as a horizontal line; the numeric estimate (95% CI) in a right column. Weight (%) shown numerically, not only by square size. - **Pooled diamond**: the random-effects summary, its width = the summary 95% CI. State the **model** (e.g., DerSimonian–Laird or REML random-effects) in the caption. - **Prediction interval**: show it (a bar through the diamond) whenever k is sufficient — it conveys where a *new study's true (study-specific) effect* would likely fall, which the summary CI (a confidence interval for the pooled mean) does not. Its absence is the single most common forest-plot omission. - **Heterogeneity line**: report **I², τ² (or τ), and Cochran's Q with its p** beneath the diamond — τ² is the actual between-study variance and must not be left implicit. - **Reference line** at the null (OR/RR/HR = 1, or 0 for mean differences); a log scale for ratio measures so CIs are symmetric. - **Axis**: framed to include all CIs without wasted whitespace; label the effect measure and which direction favours which arm. ## Discipline (what the figure must not do) - **Do not pool when pooling is not defensible**: when heterogeneity is extreme *and unexplained* (a high I² with a large τ² and no subgroup/meta-regression that accounts for it), or k is very small, a single summary can be clinically and statistically misleading — then present studies without a summary diamond, or restrict to a justified subgroup, rather than showing a precise diamond that implies an agreement the studies do not have. (I² is a guide, not an automatic cut-off — judge it with τ² and whether the heterogeneity is explained.) - **Subgroups**: plot each subgroup with its own diamond and a **test for subgroup differences**; do not narrate subgroup effects that are not shown as plotted strata. - **Small-study effects**: a funnel plot + Egger's test belong with the forest **only when k ≥ 10**; below that, say tests are underpowered rather than over-interpreting asymmetry. - **Certainty**: where GRADE is used, a certainty column (or caption note) keeps a tight CI from being read as strong evidence when risk of bias/imprecision is high. ## Common omission - A **prediction interval** and an **explicit τ²**, plus an honest **no-pool / restrict decision under extreme heterogeneity** — the elements forest plots most often skip, and the ones that turn a precise-looking diamond into an over-statement of agreement. Cross-reference `critic_rubrics/data_plot.md` §C (forest) and the SR/MA reporting items via `/check-reporting` (PRISMA). -
imaging_panel.md 3.1 KB
# Exemplar anatomy — annotated multimodality imaging panel (case report / series) A worked **anatomy model** for the composite imaging figure that carries a radiology case report or series — the figure where the diagnosis lives. This is a synthetic teaching model describing what the panel must show, not a layout to copy. Use it when the teaching point is an imaging finding, a cross-modality discordance, or a treatment response. Complements `clinical_timeline.md` (which carries chronology); this file carries the **images themselves**. ## Elements - **Panel grid with explicit labels** — one sub-panel per modality, sequence, or timepoint (e.g., mammography / ultrasound / FDG PET-CT; or T2 / FLAIR / DWI / post-contrast). Letter each sub-panel (A, B, C…) and state the modality/sequence and plane in the caption, not only the image. - **Arrow or marker to the key finding** in every sub-panel — the reader must see what to look at. An unannotated image is a decoration, not evidence. - **Quantitative labels where a number is the point** — lesion size, SUVmax, BI-RADS/standardized category, signal characteristics, degree of stenosis. These anchor the description in the text. - **Same-lesion correspondence across panels** — when showing discordance (e.g., visible on one modality, occult on another) or response (pre- vs post-treatment), keep the same lesion/orientation so the comparison is read at a glance; mark the same anatomical landmark in each. - **Modality-appropriate orientation and scale** — laterality labels (L/R), a scale where size is load-bearing, and consistent windowing across compared panels. - **Histopathology/immunostain sub-panel when origin hinges on it** — stain name and magnification in the caption (e.g., the EBER/IHC panel that settles a mimic). - **Caption that states de-identification and defines every abbreviation/scale** used in the labels. ## Discipline (what the figure must not do) - **Do not expose identifiers** — strip dates, accession numbers, institution banners, patient initials, faces, and embedded DICOM overlays before export. - **Do not show unannotated images** — every sub-panel needs an arrow/label tying it to a finding named in the case presentation. - **Do not imply causality or response without correspondence** — a "before/after" pair must be the same lesion/plane; mismatched views overstate the change. - **Do not omit the quantitative anchor** when the teaching point is a number (an "occult on PET" claim needs the SUVmax; a "stenosis" needs the degree or velocity). - **Do not overcrowd** — split into more sub-panels rather than stacking arrows on one image. ## Common omission - **Cross-modality / cross-timepoint correspondence** and the **quantitative label**. A case whose lesson is discordance or response fails if the panels are not the same lesion or the decisive number is missing. Cross-reference `write-paper/references/exemplar_case_report.md` (diagnostic-assessment and adverse-event/mimic subtypes), `clinical_timeline.md` for the chronology figure, and the CARE checklist in `/check-reporting`. -
km_curve.md 2.9 KB
# Exemplar anatomy — Kaplan–Meier survival curve A worked **anatomy model** for a publication-grade Kaplan–Meier figure, completing the figure side of the survival pair (the table side is `analyze-stats/.../table-types/survival_results.md`). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `templates/survival_analysis.py` / `survival` (R). ## Elements - **Step curves**, one per group, visually distinguishable by line style as well as colour (colourblind-safe; do not rely on colour alone). - **Number-at-risk table** aligned under the x-axis at each labelled time — the single most important KM element; without it a reader cannot judge how much of the tail is real. - **Censoring marks** (ticks on the curve at censoring times), so attrition is visible. - **Confidence band** around each curve, or at least the median's CI — a curve without uncertainty overstates precision in the sparse tail. Say which kind: a **pointwise** band covers each time separately and does *not* give simultaneous coverage over the whole curve; a **simultaneous** (e.g., Hall–Wellner / equal-precision) band does. - **Group contrast** annotated: the **log-rank p** and/or the **HR (95% CI)** with its model, and **median survival per group with 95% CI** (or "not reached"). - **Axes**: y from 0 to 1 (or 0–100%) labelled with the estimand (overall survival, PFS, …); x labelled with the time unit; a reference line is optional. ## Discipline (what the figure must not do) - **Do not extend the x-axis past where the risk set is thin.** Truncate at (or annotate) the point where estimation stops being supported — judge this by the **number at risk and the remaining events**, and by where the **CI widens sharply** (e.g., once n-at-risk falls below a small fraction of baseline) — the far tail is driven by a few patients and the steps are noise. - **Do not read a survival probability off a horizon beyond the data**; if a fixed-time estimate (e.g., 5-year survival) is quoted, the number at risk at that time must support it. - **Do not omit censoring** — heavy early censoring with a flat curve can masquerade as good survival. - For **competing risks**, a KM of one cause overestimates its incidence — use a cumulative incidence function (CIF) instead, and say so. - If proportional hazards is violated, the curves crossing is itself the message; pair with an RMST or a time-window statement rather than a single HR (see the survival table-type). ## Common omission - The **number-at-risk table** and **censoring marks** — the two elements KM figures most often drop, and the two a reviewer checks first, because both govern whether the tail can be believed. Cross-reference `critic_rubrics/data_plot.md` §C (KM) and the survival table-type in `analyze-stats/references/table-standards/table-types/survival_results.md`. -
manhattan_plot.md 4 KB
# Exemplar anatomy — Manhattan / volcano plot (agnostic many-exposure scan) A worked **anatomy model** for the figure that summarizes an **agnostic many-exposure association scan** — an exposome-/environment-/metabolome-/proteome-/nutrient-wide association study (ExWAS / EWAS / MWAS), or any "we screened N predictors" pass. Complements the `critic_rubrics/data_plot.md` §C *Manhattan / volcano* checklist (this composes; the rubric scores). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `analysis_guides/multiplicity.md` and review probe **O17** in `observational_confounding.md`. ## Which plot - **Manhattan** — x = the exposures (grouped/ordered by domain or category), y = **−log10(p)**. Best when there are many tests and you want to show *which* exposures cross threshold across domains. - **Volcano** — x = **effect size** (β / log-OR / log-HR), y = **−log10(p)**. Best when the effect *magnitude and direction* matter as much as significance — it forces effect size onto the figure so a trivially small but "significant" hit is visibly trivial. Prefer a **volcano** (or pair the two) whenever the manuscript will make an effect-size claim; a Manhattan plot alone shows significance without magnitude. ## Elements - **A significance threshold line** drawn across the plot, **and its basis named** — family-wise (Bonferroni / permutation-based study-wide threshold) for a confirmatory framing, or **FDR** (a horizontal line at the p corresponding to the q-cutoff) for discovery — with the **number of tests `m`** stated in the caption (the denominator the threshold was computed against). - **Effect size on the axis (volcano)** or a **signed direction track (Manhattan)** so up- vs down-associations are distinguishable; never significance alone. - **Sparse labelling** — annotate only the threshold-crossing / top hits; the **full tested set (every exposure with effect size + p/q) lives in a supplementary table**, not crowded onto the plot. - **Domain/category banding (Manhattan)** — group exposures (diet, chemicals, lifestyle, socio-economic …) with alternating colour bands and category labels on the x-axis, so a reader sees *where* signal concentrates. - **Replication encoded** — when the scan has a discovery + replication design, mark which hits replicated (e.g. filled = replicated, open = discovery-only) or show discovery and replication as paired panels; the caption states the replication rate. - **Grayscale-safe** — distinguish categories/direction by shape or band as well as colour. ## Discipline (what the figure must not do) - **Do not omit the threshold's basis or the test count** — a threshold line with no stated correction method and no `m` is uninterpretable; the same −log10(p) means different things at m = 30 vs m = 3000. - **Do not present a discovery-only Manhattan as confirmatory** — without replication the crossing hits are candidates; say so in the caption (frame as screening — review probe O17). - **Do not let significance stand in for effect** — a Manhattan with no effect-size channel hides that the top hit may be clinically trivial at the study's large N; use a volcano or add an effect-size panel/track. - **Do not interpret a single crossing hit causally when exposures are correlated** — it may be a marker for a correlated true cause; that is a co-exposure/mixture-model question, not something the plot resolves (`analysis_guides/multiplicity.md`). - **Do not hand-pick the plotted exposures** — plot the whole tested set; selective inclusion is the visual form of selective top-hit reporting. ## Common omission The **named threshold basis + test count `m`**, the **effect-size channel** (volcano axis or a Manhattan direction track), and the **replication encoding** — the three elements that separate a defensible agnostic-scan figure from a significance-only screen. Pair with a supplementary full-results table (every exposure, effect size, p and q). -
model_comparison_leaderboard.md 4 KB
# Exemplar anatomy — model-comparison leaderboard (across models, one cohort) A worked **anatomy model** for the figure that carries a fair **model-vs-model** comparison: several architectures / families ranked **on the same test cohort**. It is the mirror of `external_validation_comparison.md` — that one is **one model across cohorts**; this one is **many models on one cohort** — and it is the visual proof that the comparison was fair rather than a "we beat everyone" table. Pairs `design-study/references/multi_model_comparison_design.md`, `calc-sample-size/references/multi_model_comparison_sample_size.md`, and the `analyze-stats` `table-standards/table-types/model_comparison.md`. Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - The **metric per model** as a **forest / dumbbell**, one row per model, with the **strong, fairly-tuned reference baseline** as the labelled reference row — so each model is read as a **difference from the baseline**, not as an isolated number. - **A CI on every model's estimate** — a **bootstrap (BCa) CI on per-case values** for Dice (bounded and skewed near ceiling, so resample whole patients rather than trust a t-interval), a **DeLong CI** for AUC (DeLong gives the AUC's standard error and the correlated-AUC contrast — it is not a confidence band around the ROC curve). The CI width is what turns a stack of point estimates into a comparison a reviewer can judge. - **The paired Δ vs the reference** annotated per model (Δ + its **CI / significance**), because the models were run on the **same cases** — the paired difference, not two marginal numbers placed side by side, is the comparison. - **Ranking honesty**: where models are near-tied, add a **rank-stability strip** (seed-to-seed spread) or a **critical-difference** view so models the test does not separate read as **unranked** rather than ordered — and are not captioned as a demonstrated tie. - **Faceting by structure / subgroup / class** where the endpoint demands it (per-organ Dice, per-class AUC), so a model that wins on average but loses on the hard structure is visible. - **Matched-budget disclosure in the caption** — same data, patient-level split, preprocessing, and **HPO / training budget** across all models, and the number of seeds. The figure's credibility rests on this line; without it a leaderboard is unfalsifiable. ## Discipline (what the figure must not do) - **Do not bold / highlight your own row as the winner unless the paired Δ vs the runner-up excludes zero** — and read that Δ, not the overlap of the two marginal CIs, which can overlap while the paired difference is real; where the Δ does not clear zero, encode **not separated** instead of a winner. - **Do not plot one point per model with no CI or seed spread** — a single-run leaderboard ranks by *skill + luck*; a bare ranked list is a leaderboard of luck. - **Do not put models trained on different data / split / budget in the same ranking** without saying so — an unfair comparison drawn as a clean leaderboard is the core failure this figure exists to prevent. - **Do not show only the metric / facet where you win** — lead with the **pre-specified primary metric**; hiding the facets where the proposed model loses overstates the result. - **Do not rank by point estimate alone** — order by estimate but let the CIs / critical difference govern what counts as a real gap. ## Common omission - The **paired-Δ CI**, the **seed / rank-stability encoding**, and the **matched-budget caption** — the three elements a leaderboard most often drops, and the ones that separate a fair comparison from a marketing chart. The case count behind a *separable* comparison is a design-time decision (`calc-sample-size/references/multi_model_comparison_sample_size.md`). Cross-reference `external_validation_comparison.md` (across-cohort sibling), `forest_plot.md` (layout kin), `critic_rubrics/data_plot.md`, and `/model-validation` (fair comparison, tuning-on-test). -
mrmc_roc.md 3 KB
# Exemplar anatomy — MRMC reader-study ROC (multi-reader multi-case) A worked **anatomy model** for the ROC figure of a **multi-reader multi-case (MRMC)** study — the reader-study counterpart to the single-model `roc_pr.md`. The point of an MRMC figure is to show that readers are a *sample*: the curve must communicate both the reader-averaged performance and the spread across readers, so a comparison (e.g., AI-aided vs unaided, or modality A vs B) is read as generalising to the reader population, not to two specific experts. Complements `critic_rubrics/data_plot.md` §C and pairs `analyze-stats` `table-standards/table-types/reader_study.md`. Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - **Fixed 0–1 axes**, sensitivity (y) vs 1 − specificity (x), with the chance diagonal. - **Per-reader curves** (thin, one per reader) **and** the **reader-averaged curve** (bold) for each condition being compared — so the reader spread is visible, not hidden inside one mean line. - The **reader-averaged AUC with an MRMC 95% CI** (accounting for **both reader and case** variance — Obuchowski–Rockette / DBM), per condition, in the legend or panel. - For a comparison, **both conditions on the same axes** (e.g., unaided vs AI-aided) and the **ΔAUC with its MRMC CI / p**; if the design is non-inferiority, mark the **pre-specified margin**. - The **operating point(s)** actually used by readers (e.g., recommend-biopsy threshold), and a note of the **unit of analysis** (per-patient vs per-lesion) and the **fully-crossed** design. ## Discipline (what the figure must not do) - **Do not show only the reader-averaged curve** — without per-reader curves (or a reader-spread band) the figure hides whether the gain is uniform or driven by one weak reader. - **Do not quote a naïve (fixed-reader) AUC CI** for a generalising claim — the CI must reflect reader sampling (MRMC variance); a DeLong CI that ignores reader variance understates uncertainty. - **Do not pool all readers' reads as if independent**, and do not mix per-patient and per-lesion units on one curve without saying so. - **Do not imply a population-level "AI matches radiologists" claim from 2–3 expert readers** — the figure (and caption) should make the reader sample and its generalisation limit explicit. - If readers were aided by AI, **state the reading order/washout** so the comparison is not confounded by case recognition. ## Common omission - The **per-reader curves**, the **MRMC (reader+case) CI** on the averaged AUC, and the **unit of analysis / fully-crossed design** note — the elements MRMC figures most often drop, and the ones that decide whether the comparison generalises to readers rather than to the specific panel. Cross-reference `critic_rubrics/data_plot.md` §C, the diagnostic-accuracy probes `peer-review/references/domain-probes/diagnostic_accuracy.md` (D5/D6), and `analyze-stats` `table-standards/table-types/reader_study.md`. -
README.md 8.5 KB
# Exemplar plots — non-flow figure anatomy models `make-figures` carries flow-diagram exemplars (`exemplar_diagrams/{consort,prisma,stard,strobe}/`) and a non-flow **checklist** (`critic_rubrics/data_plot.md` §C: ROC, forest, KM, calibration, Bland–Altman, confusion matrix). What it lacked is a worked **anatomy model** for the non-flow figures — what a complete, publication-grade plot of each type contains, element by element. This directory fills the gap that `data_plot.md` §F ("Exemplar comparison, if exemplars exist") anticipates. These are **authored from scratch as teaching models**, not extracted from any published figure. Use them to compose a figure that has every load-bearing element, then score the draft against `critic_rubrics/data_plot.md`; do not copy an image. ## Runnable render layer (tested) Ten of the highest-yield clinical figures — **Kaplan–Meier, ROC, calibration, decision-curve, forest, Bland–Altman, confusion matrix, multi-reader ROC (MRMC), Manhattan, and clinical timeline** — have a matching **runnable, deterministic generator** in `../../scripts/render_core_figures.py`. It renders already-computed inputs (the statistical estimation stays in `/analyze-stats`; the render layer never recomputes a number) into the canonical anatomy and asserts each figure's load-bearing elements (number-at-risk table, chance diagonal, identity line, treat-all/treat-none references, no extrapolation past follow-up; forest per-study CI rows + null line + pooled diamond; Bland–Altman bias + 95% limits of agreement; confusion Predicted/Actual axes + annotated cells; MRMC per-reader + averaged curves; Manhattan significance-threshold line; timeline baseline + event markers). A network-free render-regression challenge (`scripts/render_core_figures_challenge/`, wired into `skill.yml` validation) renders all ten from a synthetic fixture and confirms the structural gate fails on a malformed figure. `imaging_panel.md` stays a **prose-only** exemplar by design: it composes real medical images with panel labels, scale bars, and arrows — an image-arrangement task, not a plot of computed numbers — so a synthetic generator would only draw placeholder boxes. Use the prose model to compose it against real images. Use the generator to produce these four directly; use the prose models below to compose the figure types that do not yet have a generator. ## Contents - `forest_plot.md` — meta-analysis forest plot: per-study square-by-weight + CI, pooled diamond with the model named, prediction interval, I²/τ²/Q, no-pool discipline under extreme heterogeneity, subgroup-difference test, funnel/Egger only at k ≥ 10. - `external_validation_comparison.md` — performance across cohorts (internal vs external / by site·scanner·vendor·sequence) with the **honest drop**: forest/dumbbell, internal as reference, per-cohort N + bootstrap-BCa CI, Δ-from-internal annotation, subgroup/structure faceting, failed cohorts marked; a suspiciously flat external result is the leakage signature. The visual of the #1 acceptance lever; pairs `/model-validation` and `calc-sample-size` segmentation-metric sizing. - `model_comparison_leaderboard.md` — model-vs-model leaderboard (**across models, one cohort** — the mirror of `external_validation_comparison.md`): one row per model with a bootstrap-Dice / DeLong CI, the **paired Δ vs a strong fairly-tuned reference baseline** with its CI/significance, ranking honesty (the paired Δ decides, not marginal-CI overlap; critical-difference / seed-stability), per-structure faceting, and a **matched-budget caption**; no "bold-our-row" cherry-pick. Pairs `design-study` multi-model-comparison design + `calc-sample-size` Test 16 + the `analyze-stats` model_comparison table-type. - `segmentation_failure_panel.md` — segmentation usability (**across cases, one model** — the third axis beside the two above): every case plotted rather than a bar of means, the **acceptability threshold drawn with the fraction below it labelled**, failures coloured by the pre-specified class (boundary drift / missed / hallucinated / catastrophic), stratification that explains the tail, and a **qualitative panel of the actual worst cases** — an anatomically impossible output is invisible in every summary statistic. Pairs `design-study` segmentation-usability design + `calc-sample-size` Test 17. - `km_curve.md` — Kaplan–Meier survival curve: number-at-risk table, censoring marks, CI band, median/log-rank/HR annotation, no extrapolation past the thin-risk-set tail, CIF for competing risks. Pairs the survival table-type. - `roc_pr.md` — ROC + precision–recall: fixed 0–1 axes, AUC CI / curve band, marked operating point, DeLong for AUC differences, PR + AUPRC (baseline = prevalence) under imbalance. - `calibration_plot.md` — calibration: predicted-vs-observed with 45° line, flexible curve, slope/intercept, predicted-risk distribution, external set, not HL-test-alone; pairs roc_pr. - `decision_curve.md` — decision curve (net-benefit/DCA): threshold-probability vs net benefit, treat-all/treat-none references, justified threshold range, calibrated model curve, proposed operating threshold; the clinical-utility counterpart to roc_pr/calibration. Pairs the `analyze-stats` incremental_value table-type. - `mrmc_roc.md` — multi-reader multi-case (MRMC) reader-study ROC: per-reader curves + bold reader-averaged curve, MRMC (reader+case) AUC CI, ΔAUC with margin, per-patient/per-lesion unit, fully-crossed/washout note. Pairs the analyze-stats reader-study table-type. - `ai_assistance_effect.md` — AI-assistance effect (reader-level paired improvement): one line per reader **unaided → aided** on the same cases, stratified by experience/specialty, reader-averaged ΔAUC/Δaccuracy with paired MRMC CI, **harmed readers marked** (automation bias), reading-time companion, crossover/washout. The within-reader-change counterpart to `mrmc_roc.md` (ROC space); pairs the analyze-stats reader-study table-type and `calc-sample-size` Test 14. - `bland_altman.md` — Bland–Altman agreement: difference vs mean-of-the-two-methods, bias line + CI, ±1.96·SD limits of agreement with CIs on each limit, proportional-bias check, % within LoA, pre-defined clinical-acceptability band, not a correlation plot, repeated-measures handling. Pairs the analyze-stats agreement table-type. - `confusion_matrix.md` — confusion matrix: TP/FP/FN/TN with explicit Predicted/Actual axes, raw counts AND row-normalized (recall) / column-normalized (precision) views, class-imbalance caveat, stated operating threshold, per-class metrics, multi-class macro-average. Pairs roc_pr. - `clinical_timeline.md` — CARE case-report timeline: relative time axis, symptoms/tests/treatment/ outcome lanes, index presentation marker, final follow-up endpoint, de-identification discipline, and annotated imaging-panel pairing when imaging is the teaching point. Pairs `write-paper` `exemplar_case_report.md`. - `imaging_panel.md` — annotated multimodality/multi-sequence imaging panel for a radiology case report or series: lettered sub-panels per modality/sequence/timepoint, arrow to the key finding in each, quantitative labels (size/SUVmax/category), same-lesion correspondence for discordance or treatment response, and de-identification discipline. Pairs `clinical_timeline.md` (chronology) and `write-paper` `exemplar_case_report.md`. - `manhattan_plot.md` — Manhattan / volcano figure for an agnostic many-exposure scan (ExWAS / EWAS / MWAS / proteome-/nutrient-wide): named significance-threshold line + the number of tests, effect-size channel (volcano axis or Manhattan direction track), sparse hit labelling with the full set in a supplement, domain banding, and replication encoding. Pairs `analyze-stats` `multiplicity.md` and review probe O17. ## Curator guidelines (for adding more) - **Synthetic only.** Describe the anatomy with placeholder specifics; never paste or trace a real figure, use no real citations, no PII, English only. - **One figure type per file**, element by element, each line stating *what the element must show* — plus a "Discipline" block of what the figure must not do and the type's most common omission. - **Complement, do not duplicate, `critic_rubrics/data_plot.md` §C** — the rubric scores; the exemplar composes. Cross-reference the rubric and the relevant `analyze-stats` template (e.g., `forest_plot.py` / `meta_analysis.R` for the forest) rather than restating them. - Keep each file ~40–60 lines. Future candidates (see `reverse_engineer/gap_register.md`): `visual_abstract` anatomy. -
roc_pr.md 2.4 KB
# Exemplar anatomy — ROC and precision–recall (PR) curves A worked **anatomy model** for ROC and PR figures, complementing the `critic_rubrics/data_plot.md` §C *ROC* checklist (this composes; the rubric scores). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy. Pairs with `analyze-stats` `templates/diagnostic_accuracy.py`. ## Elements - **ROC curve** with axes **fixed 0–1** (TPR/sensitivity vs FPR/1−specificity); a diagonal chance line; **AUROC with its 95% CI** in the panel. - **Confidence band** around the curve (bootstrap), or at least the **AUC CI** (DeLong or Hanley–McNeil for the AUC; bootstrap for the curve band) — a bare curve hides sampling error, especially at small n. - **Operating point(s)** marked on the curve — the threshold actually proposed for use, with its sensitivity/specificity; state how it was chosen (and on which data, not the test set). - **Multiple models**: distinguish by line style as well as colour; report a **test of the AUC difference** (DeLong) rather than two standalone AUCs. - **A precision–recall (PR) curve alongside ROC when classes are imbalanced** — ROC looks optimistic under low prevalence because FPR has a huge negative denominator; report **AUPRC** and note the **baseline = prevalence** (the PR "chance" line is not 0.5). ## Discipline (what the figure must not do) - **Do not read ROC as clinical performance under imbalance** — a high AUROC can coexist with a low PPV at the real base rate; pair it with PR / PPV-at-threshold (see `calibration_plot.md` for the probability side). - **Do not pick the operating point on the test set** (threshold optimism); fix it on training/ derivation data and report it. - **Do not compare AUCs without a paired test** (DeLong) on the same cases; whether the two AUCs' CIs overlap is not a valid test of their difference. - For a model that outputs probabilities, ROC/PR show **discrimination only** — discrimination is not calibration; a deployable probability still needs a calibration plot. ## Common omission - The **AUC confidence interval / curve band**, the **marked operating point**, and **AUPRC under imbalance** — the elements ROC figures most often drop, and the ones that turn a pretty curve into an overstatement of real-world performance. Cross-reference `critic_rubrics/data_plot.md` §C (ROC) and `analyze-stats` `templates/diagnostic_accuracy.py`. -
segmentation_failure_panel.md 4.5 KB
# Exemplar anatomy — segmentation failure panel (across cases, one model) A worked **anatomy model** for the figure that carries a segmentation **usability** claim: one model, one cohort, and the **distribution across cases with its tail visible**. It is the third axis of the comparison family — `model_comparison_leaderboard.md` is *many models, one cohort*, `external_validation_comparison.md` is *one model, many cohorts*, and this one is **one model, many cases**. It exists because a bar of mean Dice is the single most common way a segmentation paper shows nothing about whether the model is usable. Pairs `design-study/references/segmentation_failure_characterization_design.md` and `calc-sample-size/references/segmentation_acceptability_sample_size.md` (Test 17). Synthetic — describes *what each element must show* and the errors to avoid; not an image to copy, no real citations. ## Elements - **Every case plotted** — a **strip / jitter / dot plot** (or violin *with* the points overlaid) of the per-case metric, one point per case, faceted by structure. The mean and its CI may be drawn on top; they may not replace the points. If the reader cannot count the bad cases, the figure has not reported them. - **The acceptability threshold drawn as a line**, with the **fraction below it labelled** — the figure's actual claim ("N of M cases, 78%, met the acceptability rule"). The threshold must be the pre-specified one, and its definition belongs in the caption, not the reader's imagination. - **Failure cases marked by class** — colour or facet by the pre-specified taxonomy (boundary drift / missed structure / hallucinated structure / catastrophic). A tail of boundary drift and a tail of catastrophic outputs look identical on a metric axis and mean opposite things clinically. - **A qualitative panel of the actual worst cases** — image + contour overlay for the bottom cases, with the reference contour shown. This is not decoration: an anatomically impossible output is invisible in every summary statistic, and the small panel is the only place the reader can see whether the tail is "slightly ragged edge" or "contoured the wrong organ". - **Stratification that explains the tail** — the same distribution split by the attribute the design pre-specified (structure size, contrast phase, pathology present, scanner / site), so the figure answers *where* the model fails and not only *how often*. - **Caption carries N, the rating rule, and the raters** — number of cases, who judged acceptability and how many judged each case, and the metric's per-case definition. An acceptability fraction without its adjudication rule is not reproducible. ## Discipline (what the figure must not do) - **Do not plot a bar of means with an SD whisker** — the default segmentation figure, and the one that hides everything this figure exists to show: a mean of 0.90 over a 5% catastrophic tail draws identically to a uniform 0.90. - **Do not truncate or clip the y-axis to hide the tail** — the low outliers *are* the finding. An axis starting at 0.7 to "make the differences visible" deletes exactly the cases a reviewer wants. - **Do not report the acceptability fraction without the threshold's provenance** — a threshold set after seeing the distribution converts the figure into a post-hoc claim. - **Do not show only the structure that performed well** — facet every structure the claim covers; acceptability for one pipeline can differ by fifty points between target volumes and normal tissue, and showing the winner is the cherry-pick a reviewer flags first. - **Do not average across readers without saying so** — a per-case point that is silently a 3-reader mean, or a fraction pooled over reader×case as if independent, misstates both the distribution and its precision (Test 17). ## Common omission - The **qualitative worst-case panel**, the **failure-class encoding**, and the **threshold line with its labelled fraction** — the three elements that turn a metric distribution into a usability claim, and the ones a segmentation figure most often drops. The case count behind a *bounded* failure rate is a design-time decision (`calc-sample-size/references/segmentation_acceptability_sample_size.md`). Cross-reference `model_comparison_leaderboard.md` (across-models sibling), `external_validation_comparison.md` (across-cohorts sibling), `critic_rubrics/data_plot.md`, and `/uncertainty-imaging` when the tail is handled by abstention rather than reported as a rate.
-
-
visual_abstract_templates
-
jacc_central_illustration.pptx 28.2 KB · in bundle
-
medsci_default.pptx 28.3 KB · in bundle
-
template_guide.md 6.6 KB
# Visual Abstract Template Guide How `generate_visual_abstract.py` maps content to PPTX template shapes. ## Matching Rules The script iterates through all shapes on slide index 0 and matches by **text content** (case-insensitive substring match). Each shape is filled exactly once. | Content Field | Match Pattern (in shape.text) | CLI Flag | |---|---|---| | Article title | `ArticleTitle` | `--title` | | Hypothesis / research question | `Hypothesis` or `Question` | `--hypothesis` | | Methodology | `Methodology` or `flowchart` or `bullet` | `--methods` | | Visual element (image) | `Visual element` or `Image` or `Illustration` | `--visual` | | Main finding | `Main finding` or `relevance` | `--finding` | | Citation | `Eur Radiol` or `DOI` or `Author` | `--citation` | | Patient cohort badge | `Patient` or `cohort` | `--badges` (1st) | | Modality badge | `Modality` or `organ` | `--badges` (2nd) | | Center type badge | `Single` or `Multi-center` or `center` | `--badges` (3rd) | ## European Radiology Template — **not bundled; supply your own** European Radiology requires a graphical abstract from first revision and publishes its own template, `EURA-GA-Jan2025.pptx`. **Download it from the journal** and point the generator at it: ```bash python scripts/generate_visual_abstract.py --template /absolute/path/to/EURA-GA-Jan2025.pptx ... ``` We used to ship a copy. We no longer do, and the reason is worth stating plainly: what we were shipping was the journal's file with a **published paper's graphical abstract still filled into slide 2** — the ESR wordmark, and that paper's four-panel patient CT figure, eight images in all, inside an MIT-licensed package that anyone may copy and sell. `docProps/app.xml` still carried the article's title. It came with no licence, and nothing in this repository could see it, because the image-licence gate globbed the filesystem and a `.pptx` is a zip. A template you may download is not a template we may ship. The gate now opens containers (`scripts/check_bundled_media_license.py`), and the generator has always accepted an absolute path — so nothing is lost except a file we had no right to hand out. The shape map below still applies to the journal's file, which is why it is kept. ### Shape Map (Slide 1) | Shape # | Name | Content Field | Position | |---------|------|---------------|----------| | 0 | Title 1 | Article title | Top, full width | | 5 | Abgerundetes Rechteck 7 | Hypothesis/Question | Below title, full width | | 3 | Abgerundetes Rechteck 7 | Methodology | Left panel, below hypothesis | | 4 | Abgerundetes Rechteck 7 | Visual element (image) | Right panel, large area | | 7 | Abgerundetes Rechteck 7 | Patient cohort badge | Left, below methodology | | 8 | Abgerundetes Rechteck 7 | Modality / organ badge | Center-left, below methodology | | 9 | Abgerundetes Rechteck 7 | Single / Multi-center badge | Center, below methodology | | 6 | Abgerundetes Rechteck 7 | Main finding | Bottom area, full width | | 2 | Abgerundetes Rechteck 2 | Citation line | Bottom bar | | 1 | Content Placeholder 4 | (Logo area — leave empty or add journal logo) | ### Notes - All shape names in the EUR template are generic German ("Abgerundetes Rechteck" = rounded rectangle). The script identifies shapes by their **text content**, not by name. - The Visual element shape (Shape 4) should have its text cleared and an image inserted. The script places the image within the shape's bounding box, maintaining aspect ratio. - Badge shapes (7, 8, 9) have small icon images in the EUR example slides — the script replaces text only. Icons can be added manually in PowerPoint after generation. ## MedSci Default Template **File:** `medsci_default.pptx` **Use slide:** Index 0 A journal-neutral template following the same structure as European Radiology but without journal-specific branding. Uses neutral colors (dark gray accent, white background). ### Shape Map Same field mapping as EUR, with these differences: - No journal logo placeholder - Neutral accent color (#404040 dark gray) - Slightly wider visual element area ## Adding a New Journal Template 1. Obtain the journal's official visual abstract template (PPTX or PPT format). 2. Copy to `visual_abstract_templates/{journal_name}.pptx`. 3. Ensure the template slide has placeholder text matching the patterns in the Matching Rules table above. If not, either: - Manually edit the template to add matching placeholder text, OR - Add custom matching rules to `generate_visual_abstract.py` 4. Add a section to this guide documenting the shape map. 5. Update the journal profile in `write-paper/references/journal_profiles/` with the visual abstract requirement status and template name. ## JACC Central Illustration Template `jacc_central_illustration.pptx` — for JACC family journals (`--type central-illustration`). Built reproducibly via `scripts/build_jacc_template.py` to match the official JACC submission PPTX layout (verified against doi:10.1016/j.jacc.2019.10.035 Figures 1–4). ### Layout (10 × 7.5 in slide) | Slot | Type | Position (left, top) | Size (W × H) | Placeholder text | Filled by `--type central-illustration` | |---|---|---|---|---|---| | 1 | TEXT_BOX | (0.4, 5.3) | 9.4 × 0.5 in | `ARTICLECITATION` | `--citation` | | 2 | RECTANGLE → image | (3.0, 0.8) | 4.0 × 4.2 in | `VISUALELEMENT` | `--visual` (PNG/TIFF, ≥600 DPI) | | 3 | TEXT_BOX | (0.4, 7.0) | 4.1 × 0.5 in | `FOOTERNOTE` | optional, usually empty | | 4 | RECTANGLE | (7.3, 6.7) | 2.7 × 0.8 in | `JACCLOGO` | leave for editorial | ### Why a separate template The JACC editorial team applies the red outer border and the blue "CENTRAL ILLUSTRATION:" header bar after acceptance. Author submissions must contain only: - The content figure (slot 2) - The citation line (slot 1) Pre-rendering JACC house elements (red border, blue header) is incorrect — JACC will replace them anyway, and pre-rendered versions clash with the family branding. ### Validation rules The CI mode in `generate_visual_abstract.py` validates structural simplicity per Fuster-Mann 2019: - ≤ 3 visual zones in the content figure (`--ci-zones`) - ≤ 30 total label words (`--ci-label-words`) - ≤ 4 numerical highlights (`--ci-numerical-points`) - No methodology terms in `--ci-raw-text` (CI ≠ Visual Abstract) Override with `--ci-allow {zones|words|numerical|methods}` only when justified. ### Future cardiology templates For Circulation, EHJ, JAHA, or other journals with similar CI requirements, copy this template and rename. The Fuster-Mann 5 rules and CI mode validation apply unchanged; only the slide dimensions and citation footer pattern may need adjustment.
-
-
design_principles.md 8.2 KB
# Effective Scientific Figure Design > **Primary source**: Brunner et al., "Designing effective figures for > scientific communication." *Nat Hum Behav* (2026). > DOI: 10.1038/s41562-026-02466-9 — communication-context strategies. > > **Companion sources** (cite in figure legends / Methods when used): > - Rougier et al., "Ten simple rules for better figures." *PLoS Comput > Biol* 2014;10:e1003833 (PMID 25210732). General-purpose, foundational > ten-item checklist. > - Crameri F., "Choosing the right colors: a perceptually uniform, > colorblind-safe approach." *Curr Protoc* 2024;4:e1126 > (DOI 10.1002/cpz1.1126). Definitive 2024 reference for `viridis`, > `cividis`, `batlow` palettes and redundant encoding. > > **Triggered from**: SKILL.md Step 1 ("Specify"). Read this file **before** > choosing a figure type — it shifts focus from "which chart fits the data" to > "what message should the reader walk away with." Most figure-design guidance focuses on technical execution (axis ranges, palettes, DPI). This file complements that by adding a communication-first layer: who reads the figure, under what time pressure, and what should they remember 10 seconds later. Apply these strategies in Step 1 when specifying a figure; revisit during Step 4 / 4b when reviewing. --- ## The 5 strategies (read in order) ### 1. Identify the key message *(most important)* Before opening a plotting library, write **one sentence** describing what the figure must convey. If you cannot, the figure is premature. Examples: - "Model A outperforms Model B at every operating threshold." - "Adverse-event rates differ by route of administration but not by dose." - "The proposed pipeline runs end-to-end in under 30 seconds per case." Pin that sentence as a comment at the top of the generation script. Every panel, color, annotation, and label exists to support that message; anything that does not should be removed or moved to supplementary material. ### 2. Consider time and interaction (audience-aware) Different reading contexts allow different amounts of inspection time: | Context | Reading time | Implication | |---|---|---| | Journal article (specialist) | 30–120 s per figure | Dense detail acceptable; legend lookups OK | | Conference slide | 15–30 s, narrated | Direct labels mandatory; one message per slide | | Visual / graphical abstract | 5–10 s, no narration | One panel; minimal text; readable thumbnail | | Social-media share | 2–5 s | Self-contained; large fonts; high contrast | | Public lecture / press | 10–30 s, narrated | Plain-language axis labels; analogy via icon | Set the **reading-time budget** in Step 1, then design backwards. If the same result will appear in a journal article *and* a conference talk, build two distinct versions — the conference version usually drops half of what the journal version contains. ### 3. Choose the right graph type and use color intentionally The graph type should match the structure of the data, not aesthetic preference. | Data structure | Default | Avoid | |---|---|---| | One continuous variable, one group | Histogram, density | Pie chart | | Two continuous variables | Scatter (+ regression line if appropriate) | Bar chart | | Continuous over time | Line | Stacked bar | | Categorical proportions | Bar (sorted), waffle | 3-D pie | | Distribution by group | Box / violin / strip | Bar with SD error | | Diagnostic performance | ROC, PR curve | Single accuracy bar | | Effect size with CI | Forest plot, dot-and-whisker | Bar with asterisks | | Workflow / cohort | Flow diagram (PRISMA / CONSORT / STARD) | Free-form arrows | Color rules (compatible with this skill's `figure_specs.md`; full justification in Crameri 2024): - **Categorical groups**: Wong palette (8 colorblind-safe colors). - **Sequential magnitude**: `viridis` or `cividis` (perceptually uniform, colorblind-safe). Avoid `jet` and `rainbow` — they introduce false perceptual edges. - **Diverging around zero**: `RdBu`, `PuOr`, or `vik` (Crameri). - **Encode meaning, not decoration.** If the same conclusion holds in grayscale, color is decorative — remove or use neutral grays. - **Redundant encoding** when color carries diagnostic information: pair color with line style, marker shape, or direct label so the figure survives a deuteranopia simulation and a black-and-white print. - **Maximum 3 colors per panel** unless the data structure genuinely demands more (and then label each directly, not via legend). ### 4. Reduce cognitive load Every visual element competes for attention. The reader's working memory is roughly 7 items; design under that ceiling. - ≤7 distinct visual elements per panel (curves, boxes, annotations). - ≤3 distinct shapes (e.g., square, circle, triangle). - ≤3 colors as above. - **Direct labels on series > legend.** Legend lookups cost ~2 seconds each. - No 3-D, drop-shadow, gradient fill, or rotated axis labels unless they encode data. - Sans-serif font ≥ 9 pt at print size; ≥ 18 pt for slides; ≥ 24 pt for posters. - Whitespace is not wasted space — it groups related elements. If a panel violates two or more of these, split it into multiple panels or move detail to supplementary material. ### 5. Ask whether a figure is really needed Sometimes a table, a single sentence, or a caption-only number conveys the result more clearly. Use a figure when **at least one** of the following is true: - The reader needs to perceive a *shape* (trend, distribution, threshold). - The reader needs to *compare* across many groups simultaneously. - The result depends on a *spatial* or *anatomical* relationship. - The audience will not read the prose carefully and needs a visual hook. Otherwise, prefer a small table or in-line text. A 3-row × 2-column results table beats a bar chart of two values. --- ## Decision: figure vs table | Use a **figure** when… | Use a **table** when… | |---|---| | Trend or shape matters | Exact values matter (e.g., baseline characteristics) | | ≥4 groups / conditions | ≤3 groups *and* ≤8 metrics | | Distribution shape conveys meaning | Categorical labels with counts | | Comparison across many dimensions | Reader will reuse the numbers (re-analysis, replication) | | Visual-abstract / hero panel | Supplementary detail | When in doubt, sketch both on paper for 60 seconds and decide which the eye finishes first. --- ## Cognitive load checklist (Step 4 quick scan) - [ ] One sentence describes the key message in the script comment. - [ ] Reading-time budget matches the deployment context. - [ ] ≤7 visual elements per panel. - [ ] ≤3 colors carrying meaning (not decoration). - [ ] No 3-D / shadow / gradient unless data-driven. - [ ] Direct labels on series; legend has ≤4 entries. - [ ] Font size meets context minimum (9 pt print / 18 pt slide / 24 pt poster). - [ ] Same figure works in grayscale (run a `convert -colorspace Gray` test). If two or more boxes are unchecked, return to Step 1 before exporting. --- ## Anti-patterns (drawn from this skill's critic rubrics) These compose with the more granular checks in `critic_rubrics/data_plot.md` and `critic_rubrics/flow_diagram.md`: - **Default-palette syndrome** — using the matplotlib `tab10` palette without thought. Reads as a quick draft, not a finished figure. - **Legend-dependence** — colored series without direct labels, forcing back-and-forth between legend and data. - **Decorative 3-D** — bars or pies in 3-D with no third data dimension. - **Chart-of-three-values** — figure where a sentence would be clearer. - **Caption-as-Methods** — caption explains how the data were generated rather than what to look at; methodology belongs in the Methods section. - **Mismatched detail** — slide-deck figure rendered at 6 pt because it was copied from the manuscript without adjustment. --- ## Cross-references - `critic_rubrics/data_plot.md` — quantitative critic checks for non-flow figures - `critic_rubrics/flow_diagram.md` — extended checks for flow diagrams - `figure_specs.md` — journal-specific dimensions and DPI - `flow_diagram_lessons.md` — production lessons specific to PRISMA / CONSORT / STARD - `reporting_guideline_figure_map.md` — which figures CONSORT-AI / TRIPOD+AI / CLAIM 2024 / STARD-AI mandate - `pipeline_concepts_medical_ai.md` — DICOM workflow, annotation, federated learning, model architecture conventions -
figure_manifest.md 2.2 KB
# `figures/_figure_manifest.md` — format and field definitions Load-on-demand companion to `/make-figures`. SKILL.md states that the manifest is mandatory and who consumes it; this file is the literal format. Read it when you are writing the manifest. After generating all figures, create a structured manifest file at `figures/_figure_manifest.md`: ```markdown # Figure Manifest Generated: {YYYY-MM-DD} Study type: {study type or "custom"} | Figure | Path | Type | Tool | Critic | Rounds | Description | |--------|------|------|------|--------|--------|-------------| | Figure 1 | figures/fig1_stard_flow.svg | flow-diagram | D2 | yes | 2 | STARD participant flow diagram | | Figure 2 | figures/fig2_roc.pdf | roc-curve | matplotlib | yes | 1 | ROC curves for Model A vs B | | Figure 3 | figures/fig3_calibration.pdf | calibration | matplotlib | partial | 3 | Calibration plot; legend still crowded (see notes) | ## Critic notes - Figure 3: after 3 rounds, legend placement remains crowded at the double-column width. Candidate remediations documented but not applied to avoid reducing data-point visibility. ``` **Manifest field definitions:** - **Path**: Relative path from project root - **Type**: One of: `flow-diagram`, `roc-curve`, `forest-plot`, `funnel-plot`, `calibration`, `km-curve`, `bland-altman`, `confusion-matrix`, `box-violin`, `bar-chart`, `heatmap`, `pipeline`, `visual-abstract`, `sroc-curve`, `other` - **Tool**: Tool used to generate (`matplotlib`, `D2`, `python-pptx`, `seaborn`, etc.) - **Critic**: `yes` (all rubric items PASS) / `partial` (some PARTIAL after max rounds) / `no` (never critiqued — avoid for submission figures) / `skip` (deliberately bypassed, e.g., panel figure assembled externally) - **Rounds**: Number of Critic Loop rounds executed (0 if skipped) - **Description**: One-line description suitable for figure legend context A `## Critic notes` section at the bottom of the manifest records any residual PARTIAL items and the rationale for accepting them. This manifest is consumed by `/write-paper` Phase 2 (figure embedding) and Phase 7 (DOCX build). It **MUST** exist after figure generation completes. Verify the file is non-empty before finishing. -
figure_specs.md 11.3 KB
# Figure Specifications Reference ## Journal Dimension Requirements | Journal | Single Column | 1.5 Column | Double Column | Full Page | Max Height | |---------|--------------|------------|---------------|-----------|------------| | AJR | 3.3 in (84 mm) | 5.0 in (127 mm) | 6.83 in (174 mm) | 6.83 x 9.19 in | 9.19 in | | Radiology | 3.37 in (85.6 mm) | 5.04 in (128 mm) | 6.83 in (173.5 mm) | 6.83 x 9.19 in | 9.19 in | | Radiology: AI | 3.37 in (85.6 mm) | 5.04 in (128 mm) | 6.83 in (173.5 mm) | 6.83 x 9.19 in | 9.19 in | | European Radiology | 3.35 in (85 mm) | -- | 6.85 in (174 mm) | 6.85 x 9.13 in | 9.13 in | | KJR | 3.27 in (83 mm) | -- | 6.89 in (175 mm) | 6.89 x 9.21 in | 9.21 in | | **Default** | **3.5 in (88 mm)** | **5.0 in (127 mm)** | **7.0 in (178 mm)** | **7.0 x 9.5 in** | **9.5 in** | ### Notes - Dimensions are the maximum printable area; figures should not exceed these. - When in doubt, use single-column width (3.5 in) for simple plots and double-column (7.0 in) for complex or multi-panel figures. - Height is flexible but should not exceed the max height listed above. --- ## DPI Requirements | Content Type | Minimum DPI | Recommended DPI | Notes | |-------------|-------------|-----------------|-------| | Line art (diagrams, flow charts) | 600 | 1200 | Vector PDF preferred | | Halftone (photographs, medical images) | 300 | 300 | TIFF or PNG | | Combination (line art + halftone) | 600 | 600 | | | Color figures | 300 | 300 | | | Review/presentation | 150 | 300 | PNG acceptable | --- ## File Format Requirements by Journal | Journal | Preferred Format | Accepted Formats | Notes | |---------|-----------------|------------------|-------| | AJR | TIFF | TIFF, EPS, PDF, JPEG | TIFF LZW compression; JPEG only for photos | | Radiology | TIFF | TIFF, EPS, PDF, PNG | EPS for vector; TIFF for raster | | Radiology: AI | PDF | PDF, TIFF, EPS, PNG | PDF preferred for vector graphics | | European Radiology | TIFF | TIFF, EPS, PDF | EPS or PDF for line art | | KJR | TIFF | TIFF, JPEG, PPT | PPT accepted but not recommended | | **Default** | **PDF + PNG** | PDF (vector), PNG (raster) | Always produce both | --- ## Color Palettes ### Wong Colorblind-Safe Palette (Default) | Index | Name | Hex | Use | |-------|------|-----|-----| | 0 | Black | `#000000` | Text, axes, reference lines | | 1 | Orange | `#E69F00` | Primary category 1 | | 2 | Sky Blue | `#56B4E9` | Primary category 2 | | 3 | Bluish Green | `#009E73` | Primary category 3 | | 4 | Yellow | `#F0E442` | Highlight (use sparingly) | | 5 | Blue | `#0072B2` | Primary model/group | | 6 | Vermillion | `#D55E00` | Secondary model/group | | 7 | Reddish Purple | `#CC79A7` | Tertiary model/group | ```python WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7'] ``` ### Sequential Palettes (Heatmaps) | Use Case | Matplotlib Colormap | Direction | |----------|-------------------|-----------| | Positive values only | `Blues`, `Greens`, `Oranges` | Light to dark | | Diverging (centered at 0) | `coolwarm`, `RdBu_r` | Negative=blue, positive=red | | Correlation matrix | `coolwarm` | -1 to +1 centered | | Agreement matrix | `YlOrRd` | Low=yellow, high=red | | Confusion matrix | `Blues` | Low=white, high=blue | | p-value heatmap | `Reds_r` | Significant=dark | ### Rules - Never rely on color alone; combine with line style, marker shape, or hatching. - Test figures in grayscale to ensure readability. - Avoid pure red (#FF0000) and pure green (#00FF00) adjacent to each other. --- ## Font Size Table | Element | Size (pt) | Weight | Font Family | |---------|-----------|--------|-------------| | Figure title (rare, prefer caption) | 10 | Bold | Arial / Helvetica | | Axis label | 9 | Regular | Arial / Helvetica | | Tick label | 8 | Regular | Arial / Helvetica | | Legend text | 8 | Regular | Arial / Helvetica | | Annotation text | 8 | Regular | Arial / Helvetica | | Panel label (A, B, C) | 12 | Bold | Arial / Helvetica | | Inset text | 7 | Regular | Arial / Helvetica | | Table text (in figure) | 7-8 | Regular | Arial / Helvetica | ### Rules - Minimum readable size after print: 6 pt. - All text in figures must be embedded (not rasterized) for vector formats. - Use `plt.rcParams['font.family'] = 'sans-serif'` and `plt.rcParams['font.sans-serif'] = ['Arial', 'Helvetica']`. --- ## Common Figure Layouts ### Single Panel ``` +-----------+ | | | Plot | | | +-----------+ ``` - Width: single column (3.5 in) or double column (7.0 in) - Height: proportional, typically 1:1 or 4:3 aspect ratio - Use: ROC curve, calibration plot, KM curve, Bland-Altman ### 2-Panel Horizontal ``` +-----+-----+ | A | B | +-----+-----+ ``` - Width: double column (7.0 in), each panel ~3.3 in - Height: 3.0-3.5 in - Use: comparing two related plots (e.g., ROC for two endpoints) ### 2-Panel Vertical ``` +-----+ | A | +-----+ | B | +-----+ ``` - Width: single column (3.5 in) - Height: 6.0-7.0 in - Use: same variable, different conditions (e.g., training vs test set) ### 2x2 Grid ``` +-----+-----+ | A | B | +-----+-----+ | C | D | +-----+-----+ ``` - Width: double column (7.0 in), each panel ~3.3 in - Height: 6.0-7.0 in - Use: four related analyses (e.g., subgroup comparisons) ### 3-Panel Horizontal ``` +---+---+---+ | A | B | C | +---+---+---+ ``` - Width: double column (7.0 in), each panel ~2.1 in - Height: 2.5-3.0 in - Use: three conditions or progressive stages ### Unequal Panels (GridSpec) ``` +---------+----+ | | B | | A +----+ | | C | +---------+----+ ``` - Use `matplotlib.gridspec.GridSpec` for custom layouts - Use: main result (A) with supporting details (B, C) --- ## Caption Writing Guidelines ### Structure 1. **Sentence 1**: What the figure shows -- figure type and key finding. 2. **Sentences 2-3**: Define symbols, abbreviations, colors, and line styles. 3. **Sentence 3-4**: State sample sizes, statistical tests, significance levels. 4. **Final sentence**: Interpretation aid (if not self-evident from the plot). ### Format ``` Figure {N}. {Description starting with capital letter, ending with period.} ``` - No bold title. No title case (except proper nouns and abbreviations). - Use sentence case throughout. - Spell out abbreviations on first use in the caption. ### Multi-Panel Captions ``` Figure {N}. {Overall description.} (A) {Panel A description.} (B) {Panel B description.} ``` ### Examples **ROC Curve:** > Figure 2. Receiver operating characteristic curves for the multi-agent validation pipeline. The area under the curve was 0.92 (95% CI: 0.89-0.95) for the full pipeline and 0.84 (95% CI: 0.80-0.88) for the single-agent baseline (DeLong test, p = 0.003). The dashed diagonal line represents chance performance (AUC = 0.5). AUC = area under the curve, CI = confidence interval. **Flow Diagram:** > Figure 1. Study flow diagram following the Standards for Reporting Diagnostic Accuracy Studies (STARD) 2015 guidelines. Numbers in each box represent the count of Anki flashcards at each stage of the validation pipeline. **Multi-Panel:** > Figure 3. Agreement analysis between human reviewers and the multi-agent pipeline. (A) Bland-Altman plot showing the difference in quality scores between reviewer 1 and the pipeline against their mean. Horizontal dashed lines indicate the mean difference and 95% limits of agreement. (B) Scatter plot of reviewer 1 versus pipeline scores with the line of identity (dashed). --- ## Figure Numbering Conventions - Number figures sequentially as they appear in the text (Figure 1, Figure 2, ...). - Supplemental figures: Figure S1, Figure S2, ... - Reference every figure at least once in the main text. - Place figures after first mention (or at end, per journal preference). - Do not include a figure title inside the plot area; use the caption below instead. --- ## Flow Diagram Tool Selection (STROBE / CONSORT / PRISMA / STARD) All reporting-guideline flow diagrams use a single canonical pipeline: `scripts/generate_flow_diagram.R` (DiagrammeR DOT → DiagrammeRsvg → rsvg). ### Why this stack | Requirement | Requirement detail | DiagrammeR + rsvg | |---|---|---| | Vector PDF (editable, journal-grade) | Radiology/NEJM/Eur Radiol require EPS/AI/PDF | **True vector via `rsvg_pdf()`** | | 300 / 600 / 1200 dpi PNG | RSNA line-art = 1200 dpi; Eur Radiol = 300–1000 | **Arbitrary DPI via `rsvg_png(width=...)`** | | Arial font embedded | AMA/RSNA style | `fontname="Arial"` enforced in DOT header | | Single-color monochrome outline | BMJ/Annals IM convention | `color=black, fillcolor=white, style=filled` | | Auto-overlap resolution | Labels change size; manual coords fail | Graphviz `dot` hierarchical engine | | 4 reporting guidelines in one tool | Avoid stack sprawl | Generic DOT template switch | ### Why not the obvious alternatives | Rejected tool | Reason | |---|---| | matplotlib `FancyBboxPatch` (manual coords) | Overlap on label change; DOCX embed distortion. Root cause of an STROBE Figure 1 rework. | | D2 + post-processing | Weak Arial enforcement; PNG needs 85% vertical compression hack; font-size must be manually set 20–24. Retained as legacy fallback only. | | R `consort` v1.2.2 | CONSORT/STROBE only; STARD/PRISMA not covered; box style parameters not officially exposed (requires gpar override). | | R `PRISMA2020` v1.1.1 | `PRISMA_save()` uses webshot → PDF rasterized; no DPI parameter. Not suitable for journal submission. | | Mermaid / PlantUML | Font control weak; hard to enforce Arial + monochrome outline. | ### PRISMA 2020 compliance The generic DOT template in `generate_flow_diagram.R` implements the PRISMA 2020 structure (two identification streams, duplicates-removed box, title/abstract screening, full-text retrieval, full-text assessment, final inclusion) and can reproduce the official template shape. When a journal explicitly requires use of the PRISMA2020 R package or Shiny app for provenance, run that tool separately; the DiagrammeR pipeline is the default for all other submissions. ### File outputs Every render emits three files at the same prefix: ``` <prefix>.pdf true vector (journal submission, figure_manifest primary) <prefix>.png 300 dpi (2400 px wide; review copy, DOCX embed) <prefix>_600.png 600 dpi (4800 px wide; RSNA/Eur Radiol line-art) ``` ### System dependency `brew install librsvg` (macOS; one-time). On Linux: `apt-get install librsvg2-bin`. ## Central Illustration Dimensions Used by the JACC family and all journals that distinguish a Central Illustration from a Visual Abstract (Fuster V, Mann DL. *JACC.* 2019;74(22):2816–2820). See `references/jacc_central_illustration_principles.md`. | Spec | Value | |---|---| | PPTX slide size | 10 × 7.5 in (4:3 standard) | | Content figure area | ~4 × 4.2 in (top-center of slide) | | Citation textbox | 9.4 × 0.5 in at (0.4, 5.3) | | Content figure resolution | ≥ 600 DPI PNG; PDF vector preferred | | Font | Sans-serif (Arial or equivalent), ≥ 9 pt at print scale | | Color | Color allowed; figure should remain interpretable in grayscale | | Visual zones in content figure | 1–3 (enforced by `--type central-illustration`) | | Total label word count | ≤ 30 | | Numerical highlights | ≤ 4 | **Do not pre-render**: the red outer border and the blue "CENTRAL ILLUSTRATION:" header bar visible in published JACC issues are applied by JACC editorial after acceptance. Authors submit only the content figure and citation footer. -
flow_diagram_lessons.md 10 KB
# Flow Diagram Production — Hard-Earned Lessons > **Triggered from**: SKILL.md Step 4b (Critic Loop). Read this file when > generating PRISMA / CONSORT / STARD / STROBE diagrams, especially before a > circulation round with senior co-authors. > > **Source**: Distilled from a multi-revision meta-analysis project where > Figure 1 (PRISMA flow) went through four major iterations before clearing > a senior reviewer round. Each lesson maps to a concrete failure mode. Flow diagrams look simple but consume disproportionate revision time. The underlying causes are rarely "wrong numbers" — they are template fidelity, PDF export fidelity, and version drift between the diagram and the manuscript. The five lessons below address each in turn. --- ## Lesson 1: Use the official template once a senior reviewer is in the loop **Failure mode**: Custom Graphviz / DiagrammeR layout offers more freedom (left-aligned bullets via `\l`, `penwidth=1.8` highlights on the analytic cohort, free positioning of side-boxes). Senior systematic-review reviewers, however, expect the **PRISMA 2020 standard layout** as published by the PRISMA Statement group. A custom layout reads as "the authors did not follow the guideline," even when every count is correct. **Resolution**: - Prototype freely with Graphviz / DiagrammeR for early drafts. - **Switch to an official template before circulation.** The R package `PRISMA2020` (Haddaway et al.) renders the canonical layout from a CSV. Equivalent canonical templates exist for CONSORT 2010, STARD 2015, and STROBE. - If the official template lacks a feature you need (e.g., a third exclusion sub-branch), prefer to fold the detail into the existing slots rather than restructure. **Cross-link**: `critic_rubrics/flow_diagram.md` — "Official template fidelity" check. --- ## Lesson 2: PDF export — the VML fallback breaks under headless converters **Failure mode**: The official PRISMA 2020 docx contains VML (Vector Markup Language) fallback pairs that older Word versions render. When converted to PDF via headless LibreOffice (`soffice --headless --convert-to pdf`), the phase labels can shift by several pixels, columns misalign, and on some systems text reflows out of its box. The PDF looks "almost right" — the kind of bug that survives a quick visual scan. **Resolution by platform**: | Platform | Approach | Notes | |---|---|---| | **macOS** | AppleScript driving Microsoft Word for Mac | Deterministic native render; can be invoked headlessly via `osascript`. | | **Windows** | PowerShell / VBScript COM driving Word | `Word.Application` object → `.SaveAs2` with `wdFormatPDF`. | | **Linux / CI** | Office Online Server (paid), or commit a pre-rendered PDF produced on macOS / Windows | Headless LibreOffice is **not** safe for VML-heavy templates. | | Any platform (fallback) | Open in desktop Word → "Save As PDF" manually | Slow but always correct; document the manual step in the manifest. | **Verify visually**: After export, open the PDF and the source docx side-by-side. Compare phase-label positions, column widths, and the Y position of every numeric box. If anything has shifted, do not submit that PDF. --- ## Lesson 3: docx XML editing requires entity escape **Failure mode**: A docx file is a ZIP archive containing `word/document.xml`. Programmatic placeholder substitution (e.g., replacing `{{n_screened}}` with `1,234` plus a comment, or `{{study_title}}` with a title containing `&`) by raw `str.replace` produces invalid XML. Word then shows "We're sorry, we can't open the file because there's a problem with the contents." dialog on next open, or — worse — opens with a "repair" prompt that silently drops content. **Resolution**: - Use an entity-escaping helper before injecting any user-provided string. Python: `xml.sax.saxutils.escape(value)`. JavaScript: `he.encode` or a small regex (`& → &`, `< → <`, `> → >`). - High-risk fields: - Study titles containing `&` (`Smith & Jones, 2020`). - Range strings containing `<` or `>` (`age <5 years`, `loss >50%`). - Author lists with apostrophes (`O'Brien`). - After substitution, validate with `python -c "import zipfile, xml.etree.ElementTree as E; E.fromstring(zipfile.ZipFile('out.docx').read('word/document.xml'))"`. An exception means the docx is broken. --- ## Lesson 4: VML fallback templates need sequential placeholder maps **Failure mode**: The official PRISMA 2020 docx duplicates each numeric box as a `<w:t>` pair: a primary text element plus a VML fallback. Branch ordering in XML DOM does **not** match rendering order — the "Other" identification branch and the "Database" branch are interleaved in the document, and the VML fallbacks appear elsewhere again. A naive "replace each placeholder with the next number" loop misaligns most boxes. **Resolution**: 1. Dump every `<w:t>` in document order with a script (`python -c "from docx import Document; ..."` or direct XML walk). 2. Annotate each `<w:t>` with column / branch / role (`identification.databases.records_n`, `identification.databases.records_n.fallback`). 3. Build a static `VALUES` list with one entry per `<w:t>` slot, **with a comment per slot** documenting what it represents. 4. Validate by filling all slots with a sentinel value (e.g., `999`) and visually checking that all 60+ boxes show `999` in the rendered PDF before re-running with real data. This sentinel-render is cheap (one extra `999` pass) and catches mapping errors before they propagate into a circulation round. --- ## Lesson 5: Freeze figure versions alongside manuscript versions **Failure mode**: A reviewer asks about Figure 1 in revision round 2, but the figure file in `manuscript/figures/figure_1.pdf` was edited in place between rounds. The version visible in v3 of the manuscript no longer exists, and tracing what changed requires diffing PDFs (lossy and slow). **Resolution**: - Treat figures as part of the manuscript artifact — they freeze with each manuscript version under the **`v_N` rule** (manuscript-versioning protocol). - Maintain `figures/v{N}/` directories. Each contains the source script (`generate_figure_1.R`), the input data CSV, and the rendered docx + PDF pair. - An `INDEX.md` at `figures/INDEX.md` maps figure version ↔ manuscript version, e.g.: ``` | Manuscript | Figure 1 | Figure 2 | Figure 3 | |---|---|---|---| | v3 (circulated 2025-09-01) | figures/v3/figure_1.pdf | figures/v3/figure_2.pdf | figures/v2/figure_3.pdf | | v4 (submitted 2025-10-15) | figures/v4/figure_1.pdf | figures/v3/figure_2.pdf | figures/v3/figure_3.pdf | ``` - **Never edit `figures/v3/*.pdf` after circulation.** Branch to `figures/v4/` and rebuild. --- ## Lesson 6: Any flowchart-shaped figure uses the monochrome Graphviz convention — not colorful boxes-and-arrows **Failure mode**: A study-design / reader-flow / pipeline schematic (i.e. NOT a reporting-guideline flow) gets hand-built in matplotlib (or slides) with filled color boxes, accent palettes, and manually positioned text. It reads as "AI-generated", text is poorly aligned inside the boxes (matplotlib anchors text by point, not to the box geometry, so multi-line bodies overflow the border), and it does not match the journal house style of the group's accepted papers. **Resolution** — treat *every* flowchart-shaped figure (CONSORT/PRISMA/STARD/STROBE **and** study-design, reader-flow, cohort-assembly, MRMC/reader-study, generation- pipeline schematics) as the same object and render it with `scripts/generate_flow_diagram.R` (DiagrammeR → Graphviz `dot`). The house convention, already encoded in that script's `STYLE_HEADER`, is the journal standard: - `fillcolor=white, color=black` (white fill, **black outline only — no color**) - `fontname="Arial"`, `fontsize` 10–11 - `shape=box, style="rounded,filled"`, `penwidth=1.2` (emphasis boxes `penwidth=1.8`) - `splines=ortho` (right-angle edges), `nodesep`/`ranksep` ≈ 0.4–0.55 - left-aligned sub-lists with `\l` and `•` (`•`); side panels / exclusions via `{ rank=same; main; side }` and an invisible edge to anchor them - Graphviz lays text out *relative to the node box*, so alignment and centering are automatic — this is why it beats hand-positioned matplotlib/slide boxes. Reconcile every count to the manuscript with `stopifnot()` assertions in the script header (e.g. `stopifnot(N_auth + N_v16 + N_v12 == N_pool)`); leave data-dependent counts as a single find-replaceable token (e.g. `[N_COMPLETED]`) filled at data lock. **Do not** reach for matplotlib/PowerPoint/AI-image tools for a box-and-arrow figure. matplotlib is for *data* figures (ROC, forest, calibration, KM); Graphviz is for *flow/structure* figures. Mixing them produces the colorful, misaligned look this lesson exists to prevent. > Motivation: a reader-study Figure 1 was first built in colorful matplotlib and > rejected by the author as "too AI-looking, text not aligned in the boxes." Rebuilt > with `generate_flow_diagram.R` in the monochrome house style — matching the same > style used across the group's accepted meta-analysis, cohort, and reader-study flow > figures — it passed immediately. --- ## When to use which approach | Stage | Tool | Output | Risk if you skip | |---|---|---|---| | Early draft | DiagrammeR + Graphviz custom | PNG, fast iteration | None — flexibility wins here | | Internal QC | R `PRISMA2020` package or equivalent | Official-template PNG / PDF | Custom layout will be flagged in circulation | | Circulation | Filled official-template docx + native-Word PDF (macOS AppleScript / Windows COM) | Frozen docx + PDF pair | Headless export drift; reviewer notices misaligned labels | | Submission | `figures/v_N/` frozen pair | Immutable docx + PDF | Edit-in-place breaks the manuscript-figure version map | --- ## Quick cross-references - `critic_rubrics/flow_diagram.md` — extended critic checklist (rounds T=1…3) - `design_principles.md` — communication-first design strategies - `figure_specs.md` — journal-specific dimensions and DPI - `exemplar_diagrams/{prisma,consort,stard,strobe}/` — reference layouts -
flow_diagram_recipe.md 5 KB
# Flow diagrams (STARD / CONSORT / PRISMA / STROBE) — the R recipe Load-on-demand companion to `/make-figures`. SKILL.md states the mandatory rule (the standardized R pipeline, never matplotlib `FancyBboxPatch`, never D2 for new diagrams); this file is the recipe: the YAML schema, the fixed style, the per-project `create_figure1.R` pattern, and the legacy D2 fallback. Read it when you are actually generating a reporting-guideline flow diagram. A figure set with no flow diagram (a ROC curve, a forest plot, a calibration plot) needs none of it. **Flow diagram generation rule:** STARD/CONSORT/PRISMA/STROBE flow diagrams **MUST** use the standardized R pipeline `scripts/generate_flow_diagram.R` (DiagrammeR + Graphviz dot + rsvg). This is the single canonical tool for all four reporting-guideline flow diagrams. Do NOT use matplotlib `FancyBboxPatch` (manual coordinates break when text changes, and patches distort when embedded in DOCX). Do NOT use D2 for new flow diagrams (font control is weak, overlap requires manual post-processing). The legacy D2 recipe remains documented below as a fallback only when R is unavailable. **R flow diagram recipe (mandatory for all flow diagrams):** The pipeline reads a YAML config describing nodes/edges and produces: a true vector PDF (journal submission), a 300 dpi PNG (review copy), and a 600 dpi PNG (RSNA/Eur Radiol line-art). Default style is single-color black outline with white fill in Arial, overriding D2's colored defaults and matplotlib's manual coordinates. ```bash # 1. One-time system dependency: brew install librsvg Rscript -e 'install.packages(c("DiagrammeR","DiagrammeRsvg","rsvg","yaml"))' # 2. Author a YAML config. Templates for each type live at # references/exemplar_diagrams/{strobe,consort,prisma,stard}/template_input.yaml # 3. Render: Rscript ${CLAUDE_SKILL_DIR}/scripts/generate_flow_diagram.R \ --type {strobe|consort|prisma|stard} \ --config path/to/counts.yaml \ --out figures/figure1_flow # Outputs: figure1_flow.pdf, figure1_flow.png (300 dpi), figure1_flow_600.png ``` **YAML schema highlights:** - `rankdir: TB` (top-down, default) or `LR` (left-to-right). - `nodes:` list with `id`, `label` (use literal `\n` for line breaks, real Unicode `–`, `≤`, `−`, `•`). - Optional per-node: `highlight: true` (thicker border), `shape: note` (side boxes), `rank_same_with: <other_id>` (place on same horizontal rank). - `edges:` list with `from`, `to`, optional `style: dashed`, `arrow: false` (no arrowhead), `constraint: false` (edge ignored by layout engine — use for exclusion side-links). - Numbers in labels **MUST** be CSV-derived in an upstream R script that emits the YAML, or hand-written only when the value lives in a commit-tracked data artifact. Follow numerical-safety rules. **Style is fixed (do not override in the YAML):** - Monochrome: all boxes `color=black, fillcolor=white, fontname="Arial"`. - Penwidth 1.2 default, 1.8 for highlighted cohort box. - Arrow style: black solid, arrowsize 0.75. Dashed without arrowhead for exclusion side-links. - Bullet alignment in multi-item labels: Graphviz `\l` (left-align), never `\n` (center). Each `\l` applies to text preceding it. - **No HTML-like labels** (`label=<...>` with `<B>`, `<I>`, `•`). Plain quoted labels with `\l` bullets produce tighter, more readable structure than HTML ragged wrapping. Do not reintroduce without explicit approval. - To add one emphasis color (e.g., Wong blue `#0072B2` for a single highlighted box), edit `scripts/generate_flow_diagram.R` — do not inline hex colors in YAML. **Per-project `create_figure1.R` pattern (preferred for complex flows):** When the flow has derived counts, `stopifnot()` reconciliation, multi-rank `{rank=same; ... }` constraints, or exclusion side-cars that the generic YAML dispatcher cannot express cleanly, write a per-project `create_figure1.R` directly (same DiagrammeR + DiagrammeRsvg + rsvg stack, sprintf'd `dot` string). This is the dominant pattern when the generic YAML dispatcher cannot capture the flow: - STROBE cohort: `<project>/manuscript/figures/create_figure1.R` - STARD: `<project>/Analysis/figures/create_figure1.R` or `<project>/figures/v2_monochrome/create_figure1.R` - PRISMA / PRISMA-DTA: `<project>/5_Figures/create_figure1.R` or `<project>/analysis/create_figure1.R` - CONSORT-edu (naturalistic allocation): `<project>/figures/v2_monochrome/create_figure1.R` Copy the `STYLE_HEADER` (graph/node/edge attrs) verbatim from any exemplar; then customise nodes, edges, and `{rank=same}` blocks. Use `read.csv()` for cohort counts when possible; if hardcoded, every number must have a source comment referencing manuscript line / CSV cell / screening log row. **Legacy D2 fallback (only when R unavailable):** ```bash d2 --layout elk --theme 0 --pad 20 flow.d2 /tmp/raw.png --scale 2 # Resize + 85% vertical compression via Pillow; then render PDF: d2 --layout elk --theme 0 --pad 20 flow.d2 figures/fig1_flow.pdf ``` Use `font-size: 20-24`, `stroke: black`, `fill: white`. D2 PDF is vector; D2 PNG needs the resize step to match publication density. --- -
jacc_central_illustration_principles.md 5.8 KB
# JACC Central Illustration — Principles & Layout Spec Distilled from Fuster V, Mann DL. **The Art and Challenge of Crafting a Central Illustration or Visual Abstract.** *J Am Coll Cardiol.* 2019;74(22):2816–2820. doi:10.1016/j.jacc.2019.10.035 This document is the canonical reference when generating Central Illustrations for JACC, JACC: Asia, JACC: Cardiovascular Imaging, JACC: Heart Failure, JACC: Basic to Translational Science, JACC: CardioOncology, and JACC: Clinical Electrophysiology. ## CI vs Visual Abstract — they are different artifacts | Aspect | Central Illustration | Visual Abstract | |---|---|---| | Purpose | Single key finding / take-home message | Methods + Results pictorial summary | | Where referenced in paper | End of Results / start of Discussion | Beginning of paper | | Methods content | **None** | Required | | Audience | Cardiovascular clinicians + readers of the journal issue | Broad including non-specialists / social media | | Used by | All JACC family journals + JACC: Asia | Originally JACC: Basic to Translational Science | | Text density | Minimal (graphical priority) | More allowed (methods labels) | | Bar graphs | OK if they capture entire message | Avoid — use ↑↓ arrows | | Default complexity | Simple (1–3 visual zones) | Simple (Q→M→R three blocks) | If a paper requires *both* a central illustration and a visual abstract (some JACC sister journals), they must convey complementary content; do not duplicate. ## Five Fuster-Mann rules (must pass all) 1. **Know the message.** Decide one finding the figure must convey. Do not stuff in the study design + multiple findings + take-homes — that "causes confusion and excess that does not inform the reader." 2. **Convey graphically, not textually.** Even a simple Kaplan–Meier curve is acceptable if it captures the entire message. Bar graphs are tolerated if they earn their space. 3. **Avoid using too much text.** Any text that can be replaced by an icon or arrow should be. 4. **Avoid secondary messages.** If a viewer cannot state the main finding within 5 seconds of looking at the figure, the figure has too much. 5. **Simplicity is superior.** Authors "truly struggle with the concept" early; default to fewer panels rather than more. ## CI mode validation rules (enforced by `generate_visual_abstract.py --type central-illustration`) A submission rejects with a warning if any of the following holds: | Rule | Threshold | Rationale (Fuster-Mann) | |---|---|---| | `n_visual_zones` | ≤ 3 | "Simplicity is superior" | | `total_label_word_count` | ≤ 30 | "Avoid using too much text" | | `methods_terms_present` | none of: `cohort flow`, `inclusion`, `exclusion`, `study design`, `enrollment`, `randomized`, `sample size` | CI ≠ VA; methods belong in VA | | `numerical_data_points` | ≤ 4 | "Avoid incorporating secondary messages" | Override a single rule with `--allow rule=name` and a justification note recorded in the output PPTX speaker notes. ## JACC PPTX layout (verified from official submission templates) Reference: 4 official JACC PPTX submission files (Figures 1–4 of Fuster-Mann editorial, doi:10.1016/j.jacc.2019.10.035). All four share an identical layout: | Slot | Type | Position (left, top) inches | Size (W × H) inches | Content | |---|---|---|---|---| | 1 | TEXT_BOX | (0.4, 5.3) | 9.4 × 0.5 | Citation: `"FirstAuthor et al. JACC YYYY; vol(issue):pages."` | | 2 | PICTURE | (3.0, 0.8) | 4.0 × 4.2 | **Author-provided content figure** (the only thing the author owns) | | 3 | TEXT_BOX | (0.4, 7.0) | 4.1 × 0.5 | Reserved (often empty in production templates) | | 4 | PICTURE | (7.3, 6.7) | 2.7 × 0.8 | JACC family logo (placeholder; supplied by editorial) | Slide size: **10 × 7.5 inches (4:3)**. Background white. The red border + the blue "CENTRAL ILLUSTRATION:" header bar visible in published JACC issues are **applied by the JACC editorial team after acceptance** — authors should not pre-render those elements in their submission. Authors submit: - Slot 1 (citation text) - Slot 2 (the content figure as PNG/TIFF, ≥600 DPI, content area ~4 × 4.2 in at print) Slots 3 and 4 stay as placeholders. ## Author-provided content figure spec | Spec | Value | |---|---| | Aspect ratio | ~1:1 to slightly portrait (4 × 4.2 in) | | Print resolution | 600 DPI for PNG, vector preferred (PDF) | | Font | Sans-serif; minimum 9 pt at print scale | | Color | Allowed; high-contrast for grayscale fallback | | Visual zones | 1–3 (rule above) | | Total label words | ≤ 30 | | Numerical highlights | ≤ 4 | | Photographs | Allowed (Fuster-Mann Figure 4 example) | Reference good examples cited in the editorial: - *Moccetti F, et al. J Am Coll Cardiol. 2018;72(9):1015–26.* — heart anatomy + 3 concept boxes + vessel cross-section + brain image. Total ~20 words. (Fuster-Mann Figure 1) - *Brugada J, et al. J Am Coll Cardiol. 2018;72(9):1046–59.* — 3-column "Diagnosis / Pathophysiology / Management" grid with ECG, sequencing, defibrillator imagery. (Fuster-Mann Figure 2) ## Common author mistakes (rejected examples) - Embedding the cohort flow / CONSORT-style diagram → that is a study Figure 1 or a Visual Abstract, never a Central Illustration. - Side-by-side forest plot + KM curve + heat map → too many secondary messages. - Text-heavy "Clinical takeaway" tile boxes summarizing multiple bullets → reduce to one icon-anchored sentence or move to Discussion narrative. - Multiple HR/CI/p-value annotations across rows → keep ≤ 4 numerical highlights total. - Methodology labels ("Inclusion criteria", "N=...") → CI must not look like Methods. ## Adding a non-JACC cardiology CI template Other journals using a CI-style figure (Circulation family, EHJ, JAHA) tend to follow the same simplicity rules. For these, reuse the Fuster-Mann 5 rules above and the validation thresholds, but adjust the PPTX template to the journal's published submission size and citation footer pattern. -
medical_illustration_sources.md 4 KB
# Medical Illustration Sources Reference for finding medical illustrations for visual abstracts, graphical abstracts, and manuscript figures. Consult this file when a standalone illustration (not a data plot) is needed. ## Priority Order 1. **Study's own figures** — ROC curve, flow diagram, representative images from the study. Always preferred. No licensing issues. Most relevant to the paper. 2. **Free illustration libraries** — Download and insert into the visual abstract template. 3. **Manual drawing** — Simple shapes in Figma, PowerPoint, or Keynote. 4. **AI generation** — Only if GEMINI_API_KEY is available. Use `generate_image.py --style medical`. --- ## Free Resources (CC BY or Public Domain) ### Servier Medical Art (SMART) - **URL:** https://smart.servier.com/ - **License:** CC BY 4.0 (attribution required) - **Assets:** 3000+ medical illustrations - **Categories:** Anatomy (11 systems), cellular biology, medical specialties, equipment - **Format:** PowerPoint (editable vectors), PNG - **Best for:** Organ systems, cells, molecules, medical devices, surgical instruments - **Attribution:** "Illustrations adapted from Servier Medical Art (https://smart.servier.com/), licensed under CC BY 4.0" - **Access:** Browse categories on website. No API. Download slide sets per category. ### NIAID Visual & Medical Arts (BioArt) - **URL:** https://bioart.niaid.nih.gov/ - **License:** Public domain (US Government work — no attribution legally required, but recommended) - **Assets:** 2000+ illustrations - **Categories:** Viruses, bacteria, parasites, anatomy, cells, lab equipment, animals - **Format:** SVG, PNG, TIFF (high resolution) - **Best for:** Infectious disease, pathogens, immune cells, laboratory scenes - **Access:** Browse/search on website. No API. ### BioIcons - **URL:** https://bioicons.com/ - **License:** Various CC licenses (check per icon) - **Format:** SVG - **Best for:** Schematic diagrams, pathway illustrations, icons for flow diagrams ### Reactome Icon Library - **URL:** https://reactome.org/icon-lib - **License:** CC BY 4.0 - **Format:** SVG, PNG - **Best for:** Molecular pathways, biochemistry, cell signaling --- ## Paid Resources ### BioRender - **URL:** https://biorender.com/ - **License:** Subscription (academic plans available, ~$99/year student) - **Best for:** Professional graphical abstracts, pathway diagrams, figure panels - **Note:** Industry standard. Output must include BioRender watermark on free tier. Academic publications require paid license for copyright clearance. ### Medi-Sketch - **URL:** https://www.medi-sketch.com/ - **License:** Per-illustration purchase - **Best for:** High-quality Korean medical illustrations, graphical abstract commissions - **Note:** Korean marketplace connecting researchers with professional medical illustrators --- ## Keyword → Source Mapping | Need | Recommended Source | |------|--------------------| | Organ anatomy (heart, lung, brain, kidney) | Servier Medical Art | | Cell biology (membrane, organelles, DNA) | Servier Medical Art | | Infectious agents (virus, bacteria) | NIAID BioArt | | Lab equipment (microscope, pipette, scanner) | NIAID BioArt or Servier | | CT/MRI scanner illustration | Servier Medical Art | | Molecular pathway | Reactome or BioRender | | Schematic flow icons | BioIcons | | Custom professional illustration | Medi-Sketch or BioRender | --- ## Usage Notes - **Editable vectors preferred.** Servier provides PowerPoint files with ungroupable vector shapes. Extract individual elements and recolor to match your visual abstract palette. - **Resolution check.** Ensure downloaded PNGs are ≥300 DPI for print. SVGs scale infinitely. - **Consistency.** Within one visual abstract, use illustrations from the same source to maintain visual coherence (mixing Servier flat style with NIAID 3D renders looks inconsistent). - **AI generation warning.** See the AI-Generated Figure Warning section in SKILL.md. AI-generated medical illustrations are recognizable to reviewers. Use sparingly and customize. -
pipeline_concepts_medical_ai.md 9.2 KB
# Pipeline & Architecture Diagrams — Medical AI / Engineering > **Triggered from**: SKILL.md Step 1 (Specify) when the figure is a > pipeline / architecture / workflow for a medical AI or imaging > engineering paper. Read alongside `design_principles.md` (key-message) > and `flow_diagram_lessons.md` (production lessons). This file covers the four most common diagram types in medical-imaging AI and engineering manuscripts that the standard reporting-guideline flows (PRISMA / CONSORT / STARD / STROBE) do not address: DICOM workflow, annotation pipeline, federated-learning topology, and model architecture. For each, this file gives the canonical layout, common pitfalls, and a preferred tool. --- ## 1. DICOM workflow diagram **Use when**: the manuscript describes how DICOM images flow from scanner → PACS → research environment → preprocessing → model. **Canonical layout** (left-to-right): ``` [Modality / scanner] → [PACS] → [Research VNA / mirror] ↓ ↓ acquisition de-identification parameters + DICOM tag scrub ↓ [Preprocessed cohort store] ↓ [Train / tune / test split] ↓ [Model input] ``` **Required annotations**: - Modality (CT, MR, US, X-ray, OCT) and key acquisition parameters (kVp / mAs for CT, sequence and TE/TR for MR). - De-identification step explicitly named — DICOM tag list (e.g., remove PatientName, PatientID, AccessionNumber, InstitutionName) or reference to a published profile (DICOM PS3.15 Annex E "Basic Profile"). - Whether pixel-data redaction (burned-in PHI) was performed. - Cohort filtering criteria with counts at each step (this is also a STARD / TRIPOD requirement). **Common pitfalls**: - Drawing "PACS → model" with no de-identification box (privacy reviewers reject). - Counts only at the final analytic cohort, not at each filter step (reproducibility reviewers reject). - Using "DICOM" as a single block when the manuscript actually transforms to NIfTI / NRRD partway through — show the conversion explicitly. **Preferred tool**: D2 with `--layout elk` for left-to-right; or Graphviz `rankdir=LR`. Avoid matplotlib `FancyBboxPatch` (manual coordinates break when text changes). --- ## 2. Annotation / labeling pipeline **Use when**: the manuscript reports a labeled dataset built by human annotators (segmentation, classification, bounding boxes). **Canonical layout**: ``` [Raw image cohort] ↓ [Annotation tool / platform] ← [Annotator pool: N readers, expertise] ↓ [Round 1 labels] → [QC / consensus] → [Adjudication: senior reader] ↓ [Final reference standard] ↓ [Inter-rater agreement: κ / Dice] ``` **Required annotations**: - Number and expertise level of annotators (e.g., "3 board-certified radiologists, 5–18 years of post-board experience"). - Annotation tool (e.g., 3D Slicer, ITK-SNAP, MD.ai, RIL-Contour, custom). - Consensus rule (majority vote / unanimous / senior arbitration). - Inter-rater agreement metric and value reported separately. - Whether annotators were blinded to model output / clinical history. **Common pitfalls**: - Drawing "labeled by experts" with no count / qualification (reviewers ask for quantification). - Omitting the QC arrow (looks like single-pass labeling, low quality). - Conflating "reference standard" with "ground truth" — reference standard is the imperfect human label; ground truth is what we wish we had. **Preferred tool**: D2; nodes shaped as rectangles for steps, dashed border for human-in-the-loop steps to distinguish from automated. --- ## 3. Federated-learning topology **Use when**: the manuscript trains a model across multiple sites without centralizing raw data. **Canonical layout** (radial, central server in middle): ``` [Site A: cohort A_n, scanner type] ↑↓ (model weights only, not images) ↑↓ [Central aggregator / parameter server] ↑↓ [Site B: cohort B_n] ←→ [Site C: cohort C_n] ←→ [Site D: cohort D_n] ``` **Required annotations**: - Per-site cohort size, scanner / vendor, demographic summary. - What is exchanged (gradients / weights / encrypted updates) — and what is **not** (raw images, intermediate features). - Aggregation algorithm (FedAvg / FedProx / FedBN / etc.). - Number of communication rounds and local epochs per round. - Privacy / security mechanism (differential privacy noise, secure aggregation) if claimed. **Common pitfalls**: - Bidirectional arrows without labeling what flows each way. - No site-level demographic table — federated claims fall flat without evidence of distribution shift across sites. - Labeling raw images crossing site boundaries — if that happens it is not federated learning, it is centralized. **Preferred tool**: D2 with explicit `near: center` for the aggregator node, then sites at compass points. Or matplotlib polar layout for manuscripts that want radial symmetry. --- ## 4. Model architecture diagram **Use when**: the manuscript proposes or modifies a neural-network architecture and the message depends on the structural change. **Canonical layout** (left-to-right block diagram): ``` [Input: 3D volume H×W×D, 1 channel] ↓ [Backbone: ResNet-50 / ViT-B / nnU-Net] (cite + checkpoint) ↓ [Neck / feature pyramid / skip connections] ↓ [Head: classification / segmentation / detection] ↓ [Output: class probability / mask / bounding boxes] ``` **Required annotations**: - Input shape (H × W × D × C) and modality. - Backbone family + variant + initialization (random / ImageNet / domain-pretrained — cite the pretraining work). - Output shape and post-processing (softmax / argmax / non-max suppression). - Loss function (cross-entropy / Dice / focal / compound). - Trainable parameter count. **Style conventions** (medical-AI / engineering): - Convolutional / linear blocks: rectangles with channel count below. - Feature maps: trapezoids that shrink/grow to convey resolution change (optional but well-recognized). - Skip connections: dashed arrows. - Attention or transformer blocks: rectangles with internal split into Q / K / V annotation only when the message depends on attention. **Common pitfalls**: - "We used ResNet-50" with no diagram — fine for a methods paper, not for an architecture-contribution paper. - 3-D rendered "Convolution" pictograms with no dimension info — looks decorative, says nothing. - Drawing every layer when only the modified blocks matter — the eye cannot tell what changed. Show the modification, abstract the rest as "ResNet-50 backbone (frozen)". **Preferred tools**: - **Diagrams.net (drawio)** — fastest for one-off architecture figures. - **NN-SVG** (https://alexlenail.me/NN-SVG/) — generates clean SVG for fully-connected, LeNet, AlexNet variants. - **PlotNeuralNet** (LaTeX/TikZ) — used in the academic CV/ML community for publication-quality 3-D block diagrams. - D2 with `shape: hexagon` / `shape: cylinder` if the architecture is unusual enough that custom blocks are needed. For medical-AI papers targeting *Radiology AI*, *npj Digital Medicine*, or *Nature Medicine*, prefer NN-SVG or PlotNeuralNet output over hand-drawn blocks — reviewers in this venue are sensitive to alignment quality. --- ## When to use which (quick selector) | The figure is showing… | Use this section | |---|---| | How images move from scanner to model input | DICOM workflow | | How a labeled dataset was built | Annotation pipeline | | Multi-site training without raw-data sharing | Federated topology | | The structure of the proposed neural network | Model architecture | | Cohort filtering with counts at each stage | `flow_diagram_lessons.md` (PRISMA / STARD style) | | Training / tuning / test data splits | dataset-flow (see `reporting_guideline_figure_map.md` §AI-specific) | If the figure tries to do two of the above at once, split into two panels or two figures — combining DICOM workflow + architecture in one panel violates the cognitive-load budget (`design_principles.md` §4). --- ## Cross-references - `design_principles.md` — communication-first checks - `flow_diagram_lessons.md` — production lessons (template fidelity, PDF export, version freeze) - `reporting_guideline_figure_map.md` — which figures CLAIM / TRIPOD+AI / STARD-AI mandate - `critic_rubrics/data_plot.md` — calibration / fairness / colorblind checks - `exemplar_diagrams/pipeline/*_why.md` — design notes (the rendered figures were cropped from published papers and were removed; an MIT-licensed package cannot redistribute them) — worked exemplars (multimodal MLLM, contrastive learning, VQA, report generation) -
reporting_guideline_figure_map.md 7 KB
# Reporting Guideline → Figure Requirements Map > **Bridge**: this file connects `/make-figures` to `/check-reporting` > (49 reporting guidelines). Each row tells you which figures the guideline > **mandates** and how this skill currently supports them. Use during > Step 1 (Specify) once the study type is known. When `/check-reporting` flags missing figures for a target guideline, look up the row here and either render the supported template or fall back to the documented manual approach. --- ## Mandatory-figure map Legend for **Status** column: - ✅ Official template + R/Python generator shipped with this skill - ⚠️ Generic flow generator (`generate_flow_diagram.R`) covers the layout but no official-template fidelity check - ❌ No template; user produces with D2 / Graphviz / Inkscape from the guideline document, then runs critic_rubric manually | Guideline (year) | Study type | Mandatory figure(s) | Status | Source / where the official asset lives | |---|---|---|---|---| | **PRISMA 2020** | Systematic review | Flow diagram (4-phase: identification → screening → eligibility → included) | ✅ | `templates/official/prisma2020/`; R `PRISMA2020` package | | **PRISMA-DTA** | DTA systematic review | Modified PRISMA flow + DTA-specific exclusion reasons | ⚠️ | Salameh et al., *BMJ* 2020 (PMID 32312813); use generic flow + extra columns | | **PRISMA-NMA** | Network MA | PRISMA flow + network plot | ❌ | Hutton et al., *Ann Intern Med* 2015; network plot via R `netmeta::netgraph()` | | **PRISMA-ScR** | Scoping review | PRISMA-ScR flow diagram (sources of evidence: identification → screening → eligibility → included; item 14) | ⚠️ | Tricco et al., *Ann Intern Med* 2018 (DOI 10.7326/M18-0850); use the generic PRISMA flow with "sources of evidence" wording | | **PRISMA-P** | Protocol of SR | (none mandated; PRISMA-S search strategy figure recommended) | ❌ | Rethlefsen et al., *Syst Rev* 2021 | | **CONSORT 2025** | RCT | Participant-flow diagram (enrollment → allocation → follow-up → analysis) | ✅ | `templates/official/consort2010/` (now CONSORT 2025); R generator | | **CONSORT-AI 2020** | AI intervention RCT | CONSORT flow extended with AI training/validation/deployment dataset boxes | ❌ | Liu et al., *Nat Med* 2020 (PMID 32908283), Fig. 1; D2 / Graphviz custom | | **STARD 2015** | Diagnostic accuracy | Flow diagram (eligible → index test → reference standard → 2×2) + ROC | ✅ | `templates/official/stard2015/`; R generator | | **STARD-AI 2025** | AI diagnostic accuracy | STARD flow + dataset-flow (training / tuning / test) + subgroup-overlaid ROC/PR | ❌ | Sounderajah et al., *Nat Med* 2025 (PMID 40954311); produce manually, see `flow_diagram_lessons.md` | | **STROBE** | Observational cohort/case-control | (Flow diagram **recommended** but not strictly mandated) | ⚠️ | von Elm et al., *Ann Intern Med* 2007; use generic flow generator | | **TRIPOD 2015** | Prediction model | Calibration plot (mandatory) + discrimination (ROC, c-stat with CI) | ✅ (data plots) | Collins et al., *Ann Intern Med* 2015 | | **TRIPOD+AI 2024** | AI prediction model | TRIPOD figures + **fairness/subgroup panels** + **dataset-flow** + **decision-curve analysis** | ❌ (subgroup, DCA) | Collins et al., *BMJ* 2024 (PMID 38636956); produce manually | | **CLAIM 2024** | Medical imaging AI | Architecture diagram (model card style) + dataset-flow + calibration + per-subgroup performance + saliency/attention | ❌ | Tejani et al., *Radiology: AI* 2024 (PMID 38809149); 44 items total | | **DECIDE-AI 2022** | AI clinical eval (Stage 1–2) | Human-AI interaction diagram + safety-signal plot + override-rate over time | ❌ (uncertain — verify in full text) | Vasey et al., *Nat Med* 2022 (PMID 35585198) | | **CHEERS 2022** | Economic evaluation | Cost-effectiveness plane + cost-effectiveness acceptability curve | ❌ | Husereau et al., *BMJ* 2022 | | **SPIRIT 2025** | Trial protocol | Schedule-of-enrollment timeline (Figure 1 in published trials) | ✅ | `templates/official/spirit2013/` (updated to 2025); see also Robinson timeline figures | | **CARE 2013** | Case report | Timeline of patient course (recommended) | ⚠️ | Gagnier et al., *J Clin Epidemiol* 2014; use `exemplar_plots/clinical_timeline.md` | | **SQUIRE 2.0** | Quality improvement | Run chart / SPC chart | ❌ | Ogrinc et al., *BMJ Qual Saf* 2016 | --- ## When the status is ⚠️ (generic flow only) The R `generate_flow_diagram.R` script handles the layout, but the layout will not match the canonical guideline document exactly. For circulation to senior co-authors, that is usually acceptable; for reviewer-facing journals where fidelity is checked (esp. in *BMJ*, *Lancet*, *Ann Intern Med*, *JAMA*), prefer the official template route documented in `flow_diagram_lessons.md` Lesson 1. ## When the status is ❌ For AI-extension guidelines (CONSORT-AI, STARD-AI, TRIPOD+AI, CLAIM 2024, DECIDE-AI), there is no shipped official template **as of 2026-05**. Production path: 1. Read the original article to extract the canonical figure layout. 2. Sketch in D2 (`flow.d2`) or Graphviz DOT, layout via ELK. 3. Render to PDF with the platform-appropriate exporter (`flow_diagram_lessons.md` Lesson 2). 4. Apply the cognitive-load + key-message-visibility checks from `critic_rubrics/flow_diagram.md` Section G. 5. Cross-check against the corresponding `/check-reporting` checklist (item-by-item). When 2 or more projects need the same custom guideline figure, propose adding a new template under `templates/official/{guideline}/` so the deterministic generator can support it. --- ## AI-specific figures most often missing (priority for new templates) These are the figures that medical AI manuscripts most often omit, ranked by how frequently a reviewer-facing checklist (CLAIM 2024 / TRIPOD+AI) flags them: 1. **Dataset-flow diagram** — patient/image counts at each split (training / tuning / internal test / external test). Required by STARD-AI, CLAIM 2024, TRIPOD+AI. 2. **Calibration plot** — supported by `critic_rubrics/data_plot.md` §C. 3. **Fairness / subgroup panel** — performance by sex / race / device / site. Required by TRIPOD+AI, CLAIM 2024. 4. **Decision-curve analysis** — net benefit vs threshold. Required by TRIPOD+AI; recommended by CLAIM 2024. 5. **Architecture diagram** — input modality → preprocessing → backbone → head → output. See `pipeline_concepts_medical_ai.md`. 6. **Saliency / attention overlay** — qualitative panels showing model attention. Required by CLAIM 2024 §4. --- ## Cross-references - `/check-reporting` skill — supports all 49 guidelines, item-level audit - `flow_diagram_lessons.md` — production lessons that apply across all flows - `pipeline_concepts_medical_ai.md` — DICOM / annotation / federated / architecture diagram conventions - `design_principles.md` — communication-first design (Nat Hum Behav 2026) - `critic_rubrics/flow_diagram.md` — extended checklist (Sections A–G) - `critic_rubrics/data_plot.md` — calibration / fairness / colorblind checks
-
-
scripts
-
export_portal_tiff_challenge
-
problem.md 1.7 KB
# Challenge — portal-ready TIFF export (LZW + RGBA→RGB white-flatten) A submission portal rejects a figure and the author cannot see why. Two facts collide at the upload button: 1. The portal accepts only `.jpeg` / `.tiff` / `.eps` — **not** the `.png` the figure was rendered as (Springer Nature SNAPP does exactly this). 2. The portal caps a figure at 25 MB (JACC: Asia). A raw, uncompressed 600-dpi **RGBA** TIFF sails past the cap; the same pixels saved **LZW-compressed** with the alpha channel **flattened onto white** are a fraction of the size — and a TIFF that keeps its alpha renders the transparent regions **black** on many production pipelines. `export_portal_tiff.py` does the conversion a human otherwise does by hand in Photoshop, and then **proves** the result is pixel-identical to that white-flatten before handing it over. ## What `verify.sh` asserts (network-free, Pillow-only) Positive — on a synthetic RGBA PNG with a transparent quadrant and a colour gradient: - the output is a **TIFF**, mode **RGB** (no alpha), **LZW**-compressed (Compression tag 5); - the once-transparent region is now **white** and the opaque pixels are unchanged; - the LZW output is strictly **smaller** than an uncompressed TIFF of the same pixels. Negative — the assertions must bite, not merely pass: - flattening the same source onto **black** yields **different** bytes, so the pixel-identity check would have caught a wrong background or an ignored alpha channel; - with `--max-mb` set below the output size, the exporter **refuses (exit 1)** rather than handing back a file that will bounce at the portal. Skips cleanly if Pillow is unavailable (the same runtime dependency every raster figure helper in this skill already carries). -
verify.sh 3.6 KB
#!/usr/bin/env bash # Deterministic verifier for the portal-TIFF export challenge (make-figures). # Network-free. Generates a synthetic RGBA PNG, exports it to a portal-ready TIFF, and # asserts the output is LZW + RGB + white-flattened + pixel-identical + smaller than raw; # then confirms the flatten and the size-cap assertions actually BITE. Exit 0 = all hold. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" GEN="$HERE/../export_portal_tiff.py" TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT [ -f "$GEN" ] || { echo "ENV-ERR: export_portal_tiff.py missing" >&2; exit 2; } python3 -c "import PIL" 2>/dev/null \ || { echo "SKIP: Pillow unavailable on this host"; exit 0; } # --- synthetic fixture: RGBA with a fully transparent top-left quadrant + a gradient ----- python3 - "$TMP" <<'PY' import sys from PIL import Image d = sys.argv[1] img = Image.new("RGBA", (200, 200), (0, 0, 0, 0)) px = img.load() for y in range(200): for x in range(200): a = 0 if (x < 100 and y < 100) else 255 # top-left transparent px[x, y] = ((x * 255) // 200, (y * 255) // 200, 128, a) img.save(f"{d}/fig.png") PY # (1) Positive: the exporter runs and self-verifies. python3 "$GEN" --in "$TMP/fig.png" --out "$TMP/fig.tiff" --max-mb 25 >"$TMP/log" 2>&1 \ || { echo "FAIL: valid figure did not export/verify" >&2; cat "$TMP/log" >&2; exit 1; } [ -s "$TMP/fig.tiff" ] || { echo "FAIL: fig.tiff not written" >&2; exit 1; } grep -q "OK: portal-ready TIFF" "$TMP/log" || { echo "FAIL: export did not report success" >&2; cat "$TMP/log" >&2; exit 1; } # (2) Independent structural assertions on the produced TIFF. python3 - "$TMP" <<'PY' import os, sys from PIL import Image d = sys.argv[1] im = Image.open(f"{d}/fig.tiff"); im.load() assert im.mode == "RGB", f"expected RGB, got {im.mode}" assert im.tag_v2.get(259) == 5, f"expected LZW (Compression 5), got {im.tag_v2.get(259)}" rgb = im.convert("RGB") assert rgb.getpixel((10, 10)) == (255, 255, 255), f"transparent quadrant not white: {rgb.getpixel((10,10))}" assert rgb.getpixel((150, 150)) == (191, 191, 128), f"opaque pixel altered: {rgb.getpixel((150,150))}" # LZW must be genuinely smaller than an uncompressed TIFF of the same pixels. rgb.save(f"{d}/raw.tiff", format="TIFF", compression="none") lzw, raw = os.path.getsize(f"{d}/fig.tiff"), os.path.getsize(f"{d}/raw.tiff") assert lzw < raw, f"LZW ({lzw}) not smaller than uncompressed ({raw})" print(f"OK-STRUCT: RGB + LZW + white-flatten + {lzw} < {raw} bytes") PY # (3) Negative — the flatten discriminates: onto BLACK yields different bytes, so the # pixel-identity check would have caught a wrong background / ignored alpha. python3 - "$TMP" "$GEN" <<'PY' import importlib.util, sys from PIL import Image d, gen = sys.argv[1], sys.argv[2] spec = importlib.util.spec_from_file_location("ept", gen) ept = importlib.util.module_from_spec(spec); spec.loader.exec_module(ept) src = Image.open(f"{d}/fig.png"); src.load() white = ept.flatten_to_rgb(src, (255, 255, 255)).tobytes() black = ept.flatten_to_rgb(src, (0, 0, 0)).tobytes() assert white != black, "flatten ignored alpha — white and black backgrounds produced identical bytes" print("OK-NEG-FLATTEN: white != black flatten (alpha is genuinely composited)") PY # (4) Negative — the size cap bites: a cap below the output size must exit 1. if python3 "$GEN" --in "$TMP/fig.png" --out "$TMP/tiny.tiff" --max-mb 0.001 >/dev/null 2>&1; then echo "FAIL: --max-mb 0.001 did not refuse an over-cap output" >&2; exit 1 fi echo "OK-NEG-CAP: --max-mb below output size refuses (exit 1)" echo "PASS: portal TIFF is LZW + RGB + white-flattened + pixel-identical + under cap; the flatten and size-cap assertions both bite."
-
-
render_core_figures_challenge
-
fixture
-
synthetic_inputs.json 3.5 KB
{ "_comment": "Synthetic, hand-authored figure inputs (no real patient data). Already-computed step/curve coordinates and summary statistics — this fixture exercises the RENDER layer, not any statistical estimation.", "km": { "title": "Synthetic two-arm survival", "max_follow_up": 24, "groups": [ { "name": "Arm A", "time": [0, 6, 12, 18, 24], "surv": [1.0, 0.92, 0.81, 0.74, 0.68], "censor_times": [10, 20], "at_risk": {"time": [0, 6, 12, 18, 24], "n": [200, 170, 140, 110, 80]} }, { "name": "Arm B", "time": [0, 6, 12, 18, 24], "surv": [1.0, 0.85, 0.70, 0.58, 0.50], "censor_times": [8, 16], "at_risk": {"time": [0, 6, 12, 18, 24], "n": [180, 150, 118, 90, 60]} } ] }, "roc": { "fpr": [0.0, 0.05, 0.10, 0.20, 0.35, 0.60, 1.0], "tpr": [0.0, 0.45, 0.62, 0.78, 0.88, 0.95, 1.0], "auc": 0.862, "operating_point": {"fpr": 0.10, "tpr": 0.62, "label": "Youden point"} }, "calibration": { "pred_mean": [0.10, 0.30, 0.50, 0.70, 0.90], "obs_freq": [0.12, 0.28, 0.52, 0.66, 0.85], "slope": 0.93, "intercept": 0.02, "ci_low": [0.06, 0.21, 0.44, 0.58, 0.77], "ci_high": [0.19, 0.36, 0.60, 0.74, 0.91] }, "dca": { "thresholds": [0.05, 0.10, 0.20, 0.30, 0.40, 0.50], "net_benefit_model": [0.34, 0.30, 0.22, 0.15, 0.09, 0.04], "prevalence": 0.35 }, "forest": { "effect_label": "Odds ratio (95% CI)", "null_value": 1.0, "log_x": true, "studies": [ {"name": "Study A 2019", "est": 1.20, "lo": 0.90, "hi": 1.60, "weight": 25}, {"name": "Study B 2020", "est": 1.45, "lo": 1.10, "hi": 1.92, "weight": 30}, {"name": "Study C 2021", "est": 0.95, "lo": 0.70, "hi": 1.29, "weight": 20}, {"name": "Study D 2022", "est": 1.60, "lo": 1.15, "hi": 2.22, "weight": 25} ], "pooled": {"est": 1.30, "lo": 1.10, "hi": 1.54, "label": "Random-effects pooled"} }, "bland_altman": { "mean_vals": [10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32], "diff_vals": [0.2, -0.5, 0.8, 1.1, -0.3, 0.6, -1.2, 0.9, 0.1, 1.5, -0.7, 0.4], "bias": 0.24, "sd_diff": 0.80 }, "confusion": { "matrix": [[85, 15], [10, 90]], "labels": ["Negative", "Positive"] }, "mrmc_roc": { "readers": [ {"name": "Reader 1", "fpr": [0.0, 0.10, 0.25, 0.50, 1.0], "tpr": [0.0, 0.55, 0.72, 0.88, 1.0], "auc": 0.80}, {"name": "Reader 2", "fpr": [0.0, 0.08, 0.20, 0.45, 1.0], "tpr": [0.0, 0.62, 0.78, 0.90, 1.0], "auc": 0.84}, {"name": "Reader 3", "fpr": [0.0, 0.12, 0.30, 0.55, 1.0], "tpr": [0.0, 0.50, 0.68, 0.85, 1.0], "auc": 0.77} ], "averaged": {"fpr": [0.0, 0.10, 0.25, 0.50, 1.0], "tpr": [0.0, 0.56, 0.73, 0.88, 1.0], "auc": 0.805, "label": "Reader-averaged"}, "delta_auc": {"value": 0.06, "margin": 0.05} }, "manhattan": { "xlabel": "Candidate exposure (index)", "threshold": 2.6, "x": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20], "neglogp": [0.4, 1.2, 0.8, 3.5, 0.6, 1.1, 2.1, 0.9, 4.2, 0.5, 1.3, 0.7, 2.8, 1.0, 0.4, 5.1, 0.9, 1.4, 0.6, 2.2], "labels": ["", "", "", "Exposure D", "", "", "", "", "Exposure I", "", "", "", "Exposure M", "", "", "Exposure P", "", "", "", ""] }, "timeline": { "time_unit": "days from admission", "events": [ {"time": 0, "label": "Admission"}, {"time": 2, "label": "CT scan"}, {"time": 5, "label": "Biopsy"}, {"time": 9, "label": "Diagnosis"}, {"time": 14, "label": "Treatment start"}, {"time": 30, "label": "Follow-up"} ] } }
-
-
problem.md 3.4 KB
# Challenge card — core clinical-figure render regression (make-figures) ## Problem The highest-yield clinical figures — Kaplan–Meier, ROC, calibration, decision-curve, forest, Bland–Altman, and confusion matrix — were documented only as **prose anatomy** in `references/exemplar_plots/`, and the actual matplotlib rendering had **no deterministic test of any kind**. A regression in figure code (a dropped number-at-risk table, a missing chance diagonal, a calibration plot without its identity line, a DCA without the treat-all / treat-none references, a KM curve extrapolated past follow-up, a forest without its pooled diamond or null line, a Bland–Altman without its limits of agreement, or a confusion matrix without annotated cells) would pass every prose read and only be caught by a reviewer — the gap that left the suite's self-identified weakest area with the same defense/enablement asymmetry the rest of the repo has closed (integrity detectors have challenge fixtures; the figure generators did not). ## What the generator does `scripts/render_core_figures.py` is the **render** layer for the exemplar anatomies. It turns each prose model into a runnable, deterministic matplotlib generator that takes **already-computed inputs** (the analysis SoT stays in `/analyze-stats`; this never recomputes a statistic) and renders the canonical anatomy. `assert_structure` then introspects the actual matplotlib artists and asserts each figure's load-bearing elements are present: - **KM** — step curve(s), number-at-risk table, monotonic non-increasing survival, x-axis clipped to follow-up (no extrapolation). - **ROC** — chance diagonal, AUC annotation, operating-point marker. - **Calibration** — identity (y = x) line, slope + intercept annotation, predicted-vs-observed axes. - **Decision curve** — model + treat-all + treat-none strategies, the treat-none (net benefit = 0) reference, a net-benefit y-axis. - **Forest** — a per-study CI whisker for every study, the null reference line, and the pooled diamond; study + pooled row labels. - **Bland–Altman** — the difference scatter, the bias line, and the 95% limits of agreement (bias ± 1.96·SD); difference-vs-mean axes. - **Confusion matrix** — a matrix image with every cell annotated and Predicted/Actual axes. - **MRMC ROC** — a curve per reader + the reader-averaged curve, the chance diagonal, and the averaged-AUC annotation. - **Manhattan** — the point scatter, the named significance-threshold line, and a −log10(p) y-axis. - **Clinical timeline** — the time baseline, an event marker + label at each event, and a time x-axis. (`imaging_panel` stays a prose-only exemplar — it composes real images, not computed numbers, so it has no synthetic generator.) ## Fixture (synthetic only — no real data) - `fixture/synthetic_inputs.json` — hand-authored coordinates and summary statistics for all ten figures. ## Expected (`verify.sh`, network-free) - All ten figures render to PNGs (each > 2 KB) **and** every structural invariant holds → exit 0. - Mutated inputs that drop a load-bearing element (a non-monotonic KM curve; a non-square confusion matrix) raise `AssertionError` → the negative cases in `verify.sh` confirm the gate actually fails when it should. Requires matplotlib + numpy (already make-figures runtime deps); the verifier skips with a clear message if matplotlib is unavailable, so it never hard-fails a minimal host. -
verify.sh 2.3 KB
#!/usr/bin/env bash # Deterministic verifier for the core-figure render challenge (make-figures). # Network-free. Renders the ten canonical clinical figures from a synthetic fixture and # asserts each figure's load-bearing elements; then confirms the structural gate FAILS on # mutated inputs (so the assertions are proven to bite). Exit 0 = all expectations hold. set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" GEN="$HERE/../render_core_figures.py" FIX="$HERE/fixture/synthetic_inputs.json" TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT [ -f "$GEN" ] || { echo "ENV-ERR: render_core_figures.py missing" >&2; exit 2; } python3 -c "import matplotlib, numpy" 2>/dev/null \ || { echo "SKIP: matplotlib/numpy unavailable on this host"; exit 0; } # (1) Positive: all ten figures render + every structural invariant holds. python3 "$GEN" --inputs "$FIX" --out-dir "$TMP/out" >"$TMP/log" 2>&1 \ || { echo "FAIL: valid fixture did not render/verify" >&2; cat "$TMP/log" >&2; exit 1; } for k in km roc calibration dca forest bland_altman confusion mrmc_roc manhattan timeline; do [ -s "$TMP/out/$k.png" ] || { echo "FAIL: $k.png not written" >&2; exit 1; } done grep -q "PASS: 10 figure" "$TMP/log" || { echo "FAIL: not all ten figures verified" >&2; cat "$TMP/log" >&2; exit 1; } # (2) Negative — the assertions must actually fire on malformed inputs. # (2a) a KM survival curve mutated to be non-monotonic. python3 - "$FIX" "$TMP/bad_km.json" <<'PY' import json, sys d = json.load(open(sys.argv[1])) d["km"]["groups"][0]["surv"] = [1.0, 0.92, 0.95, 0.74, 0.68] # 0.81 -> 0.95 (increasing) json.dump(d, open(sys.argv[2], "w")) PY if python3 "$GEN" --inputs "$TMP/bad_km.json" --out-dir "$TMP/bk" >/dev/null 2>&1; then echo "FAIL: a non-monotonic KM survival curve was NOT caught" >&2; exit 1 fi # (2b) a non-square confusion matrix (label/shape mismatch). python3 - "$FIX" "$TMP/bad_cm.json" <<'PY' import json, sys d = json.load(open(sys.argv[1])) d["confusion"]["matrix"] = [[85, 15, 0], [10, 90, 0]] # 2x3, not square json.dump(d, open(sys.argv[2], "w")) PY if python3 "$GEN" --inputs "$TMP/bad_cm.json" --out-dir "$TMP/bc" >/dev/null 2>&1; then echo "FAIL: a non-square confusion matrix was NOT caught" >&2; exit 1 fi echo "PASS: 10 core figures render + structurally verify; the gate fails on a non-monotonic KM curve and a non-square confusion matrix."
-
-
build_jacc_template.py 2.9 KB
"""Build the JACC Central Illustration PPTX template. Layout matches official JACC submission templates (verified from doi:10.1016/j.jacc.2019.10.035 Figures 1–4 PPTX files). Run once. Output: references/visual_abstract_templates/jacc_central_illustration.pptx """ from pathlib import Path from pptx import Presentation from pptx.util import Inches, Pt, Emu from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE ROOT = Path(__file__).resolve().parents[1] OUT = ROOT / "references" / "visual_abstract_templates" / "jacc_central_illustration.pptx" # JACC submission slide is 10 x 7.5 in (4:3 standard PPT) prs = Presentation() prs.slide_width = Inches(10) prs.slide_height = Inches(7.5) # Blank layout blank = prs.slide_layouts[6] slide = prs.slides.add_slide(blank) # ---- Slot 1: citation text box ---- cite = slide.shapes.add_textbox(Inches(0.4), Inches(5.3), Inches(9.4), Inches(0.5)) tf = cite.text_frame tf.text = "ARTICLECITATION — replace with: FirstAuthor et al. JACC: Asia YYYY; vol(issue):pages." p = tf.paragraphs[0] p.font.size = Pt(11) p.font.bold = True p.font.color.rgb = RGBColor(0x1F, 0x4E, 0x79) # ---- Slot 2: content picture placeholder (rectangle as marker) ---- ph = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, Inches(3.0), Inches(0.8), Inches(4.0), Inches(4.2) ) ph.fill.solid() ph.fill.fore_color.rgb = RGBColor(0xF2, 0xF2, 0xF2) ph.line.color.rgb = RGBColor(0xBF, 0xBF, 0xBF) ph.line.width = Pt(1.5) ph.text_frame.text = "VISUALELEMENT — replace this rectangle with the author content figure (PNG/TIFF, 600 DPI, ~4×4.2 in)." for para in ph.text_frame.paragraphs: para.font.size = Pt(10) para.font.color.rgb = RGBColor(0x59, 0x59, 0x59) para.alignment = 1 # center # ---- Slot 3: footer text box (reserved; usually empty in JACC templates) ---- foot = slide.shapes.add_textbox(Inches(0.4), Inches(7.0), Inches(4.1), Inches(0.5)) foot.text_frame.text = "FOOTERNOTE — optional secondary caption (often empty)." for para in foot.text_frame.paragraphs: para.font.size = Pt(9) para.font.color.rgb = RGBColor(0x80, 0x80, 0x80) para.font.italic = True # ---- Slot 4: logo placeholder (rectangle as marker) ---- logo = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, Inches(7.3), Inches(6.7), Inches(2.7), Inches(0.8) ) logo.fill.solid() logo.fill.fore_color.rgb = RGBColor(0xFF, 0xFF, 0xFF) logo.line.color.rgb = RGBColor(0xBF, 0xBF, 0xBF) logo.line.width = Pt(0.75) logo.text_frame.text = "JACCLOGO — JACC family logo (provided by editorial)." for para in logo.text_frame.paragraphs: para.font.size = Pt(8) para.font.color.rgb = RGBColor(0x80, 0x80, 0x80) para.alignment = 1 # Save OUT.parent.mkdir(parents=True, exist_ok=True) prs.save(OUT) print(f"Saved: {OUT}") print(f"Slide size: {Emu(prs.slide_width).inches:.2f} x {Emu(prs.slide_height).inches:.2f} in") print("Placeholders: ARTICLECITATION, VISUALELEMENT, FOOTERNOTE, JACCLOGO") -
build_prisma2020_template.py 11.2 KB
#!/usr/bin/env python3 """Build a PRISMA 2020 flow diagram template (.pptx) programmatically. Why this script exists ---------------------- prismastatement.org serves the official .docx templates behind a Squarespace JavaScript fingerprint redirect that blocks programmatic download (curl, wget, WebFetch, Wayback Machine all return the redirect HTML stub, not the file). We reproduce the published layout from Page MJ et al. *BMJ* 2021;372:n71 (Fig 1, "PRISMA 2020 flow diagram"), which the PRISMA Group releases under CC-BY 4.0. Box positions, labels, and arrow topology mirror the official template; only the per-line counts are placeholders that fill_prisma_template.py populates. Variants -------- - ``new`` : new systematic reviews (no "Studies included in previous version" branch) - ``updated`` : updated systematic reviews (adds the previous-version branch on the right) The "registers/databases only" (v1) vs "+ other sources" (v2) distinction is handled by toggling the ``--include-other-sources`` flag, which adds the secondary identification column on the left. Usage ----- python3 build_prisma2020_template.py \ --variant new --include-other-sources \ --out templates/official/prisma2020/PRISMA_2020_flow_new_v2.pptx """ from __future__ import annotations import argparse from pathlib import Path from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.util import Emu, Inches, Pt # ── Layout constants (inches) ──────────────────────────────────────────────── SLIDE_W = 13.33 SLIDE_H = 10.0 BOX_W = 3.4 BOX_H = 0.95 COL_GAP = 0.4 ROW_GAP = 0.45 # Phase column on the far left PHASE_COL_X = 0.3 PHASE_COL_W = 1.3 # Identification block columns IDENT_COL1_X = PHASE_COL_X + PHASE_COL_W + 0.2 # registers / databases IDENT_COL2_X = IDENT_COL1_X + BOX_W + COL_GAP # other sources (v2 only) # Vertical positions TITLE_Y = 0.25 ROW1_Y = 1.1 # records identified ROW2_Y = ROW1_Y + BOX_H + ROW_GAP # records removed before screening (right inset) SCREENING_Y = 3.3 SCREENING_EXCL_X_OFFSET = BOX_W + COL_GAP ELIGIBILITY_Y = SCREENING_Y + (BOX_H + ROW_GAP) * 2 # reports retrieved + sought INCLUDED_Y = SLIDE_H - BOX_H - 0.6 # Colors NAVY = RGBColor(0x1F, 0x3A, 0x68) LIGHT = RGBColor(0xE8, 0xEE, 0xF7) BLACK = RGBColor(0x00, 0x00, 0x00) GREY = RGBColor(0x55, 0x55, 0x55) # ── Helpers ────────────────────────────────────────────────────────────────── def add_box( slide, left: float, top: float, width: float, height: float, text: str, *, fill=LIGHT, border=NAVY, bold_first_line: bool = True, font_size: int = 10, ): """Add a rounded rectangle with the given text. Returns the shape.""" shape = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, Inches(left), Inches(top), Inches(width), Inches(height), ) shape.fill.solid() shape.fill.fore_color.rgb = fill shape.line.color.rgb = border shape.line.width = Pt(0.75) tf = shape.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.06) tf.margin_top = tf.margin_bottom = Inches(0.04) lines = text.split("\n") for i, line in enumerate(lines): para = tf.paragraphs[0] if i == 0 else tf.add_paragraph() run = para.add_run() run.text = line run.font.size = Pt(font_size) run.font.color.rgb = BLACK run.font.bold = bold_first_line and i == 0 return shape def add_phase_label(slide, top: float, text: str): shape = slide.shapes.add_shape( MSO_SHAPE.RECTANGLE, Inches(PHASE_COL_X), Inches(top), Inches(PHASE_COL_W), Inches(BOX_H), ) shape.fill.solid() shape.fill.fore_color.rgb = NAVY shape.line.color.rgb = NAVY tf = shape.text_frame tf.word_wrap = True p = tf.paragraphs[0] p.alignment = 2 # center run = p.add_run() run.text = text run.font.size = Pt(12) run.font.bold = True run.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) def add_arrow(slide, x1, y1, x2, y2): line = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2)) line.line.color.rgb = NAVY line.line.width = Pt(1.25) # ── Builders ───────────────────────────────────────────────────────────────── def build_template(variant: str, include_other_sources: bool, out_path: Path) -> None: prs = Presentation() prs.slide_width = Inches(SLIDE_W) prs.slide_height = Inches(SLIDE_H) blank_layout = prs.slide_layouts[6] slide = prs.slides.add_slide(blank_layout) # Title title_box = slide.shapes.add_textbox( Inches(0.3), Inches(TITLE_Y), Inches(SLIDE_W - 0.6), Inches(0.6) ) tf = title_box.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = ( f"PRISMA 2020 flow diagram for " f"{'updated' if variant == 'updated' else 'new'} systematic reviews" + (" (databases, registers, and other sources)" if include_other_sources else " (databases and registers only)") ) run.font.size = Pt(14) run.font.bold = True run.font.color.rgb = NAVY # Phase labels (left column) add_phase_label(slide, ROW1_Y, "Identification") add_phase_label(slide, SCREENING_Y, "Screening") add_phase_label(slide, INCLUDED_Y, "Included") # ── Identification row ─────────────────────────────────────────────────── add_box( slide, IDENT_COL1_X, ROW1_Y, BOX_W, BOX_H, "Records identified from:\n" " Databases (n = {n_db})\n" " Registers (n = {n_reg})", font_size=10, ) # Right inset: records removed before screening removed_x = IDENT_COL1_X + BOX_W + COL_GAP + (BOX_W + COL_GAP if include_other_sources else 0) add_box( slide, removed_x, ROW1_Y, BOX_W, BOX_H, "Records removed before screening:\n" " Duplicate records removed (n = {n_dup})\n" " Records marked as ineligible by automation tools (n = {n_auto})\n" " Records removed for other reasons (n = {n_other_removed})", font_size=9, ) add_arrow(slide, IDENT_COL1_X + BOX_W, ROW1_Y + BOX_H / 2, removed_x, ROW1_Y + BOX_H / 2) if include_other_sources: add_box( slide, IDENT_COL2_X, ROW1_Y, BOX_W, BOX_H, "Records identified from:\n" " Websites (n = {n_web})\n" " Organisations (n = {n_org})\n" " Citation searching (n = {n_cite})\n" " etc.", font_size=9, ) # ── Screening row ──────────────────────────────────────────────────────── add_box( slide, IDENT_COL1_X, SCREENING_Y, BOX_W, BOX_H, "Records screened\n(n = {n_screened})", ) add_box( slide, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, SCREENING_Y, BOX_W, BOX_H, "Records excluded\n(n = {n_screen_excluded})", ) add_arrow(slide, IDENT_COL1_X + BOX_W / 2, ROW1_Y + BOX_H, IDENT_COL1_X + BOX_W / 2, SCREENING_Y) add_arrow( slide, IDENT_COL1_X + BOX_W, SCREENING_Y + BOX_H / 2, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, SCREENING_Y + BOX_H / 2, ) # Reports sought / not retrieved add_box( slide, IDENT_COL1_X, SCREENING_Y + BOX_H + ROW_GAP, BOX_W, BOX_H, "Reports sought for retrieval\n(n = {n_sought})", ) add_box( slide, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, SCREENING_Y + BOX_H + ROW_GAP, BOX_W, BOX_H, "Reports not retrieved\n(n = {n_not_retrieved})", ) add_arrow( slide, IDENT_COL1_X + BOX_W / 2, SCREENING_Y + BOX_H, IDENT_COL1_X + BOX_W / 2, SCREENING_Y + BOX_H + ROW_GAP, ) add_arrow( slide, IDENT_COL1_X + BOX_W, SCREENING_Y + BOX_H + ROW_GAP + BOX_H / 2, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, SCREENING_Y + BOX_H + ROW_GAP + BOX_H / 2, ) # Reports assessed / excluded with reasons assess_y = SCREENING_Y + (BOX_H + ROW_GAP) * 2 add_box( slide, IDENT_COL1_X, assess_y, BOX_W, BOX_H, "Reports assessed for eligibility\n(n = {n_assessed})", ) add_box( slide, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, assess_y, BOX_W, BOX_H + 0.4, "Reports excluded:\n" " Reason 1 (n = {n_excl_r1})\n" " Reason 2 (n = {n_excl_r2})\n" " Reason 3 (n = {n_excl_r3})\n" " etc.", font_size=9, ) add_arrow( slide, IDENT_COL1_X + BOX_W / 2, SCREENING_Y + (BOX_H + ROW_GAP) * 2 - ROW_GAP, IDENT_COL1_X + BOX_W / 2, assess_y, ) add_arrow( slide, IDENT_COL1_X + BOX_W, assess_y + BOX_H / 2, IDENT_COL1_X + SCREENING_EXCL_X_OFFSET, assess_y + BOX_H / 2, ) # ── Included row ───────────────────────────────────────────────────────── add_box( slide, IDENT_COL1_X, INCLUDED_Y, BOX_W * 1.3, BOX_H + 0.3, "Studies included in review (n = {n_studies})\n" "Reports of included studies (n = {n_reports})", font_size=11, ) add_arrow( slide, IDENT_COL1_X + BOX_W / 2, assess_y + BOX_H + 0.4, IDENT_COL1_X + BOX_W / 2, INCLUDED_Y, ) # Footer attribution (CC-BY) foot = slide.shapes.add_textbox( Inches(0.3), Inches(SLIDE_H - 0.45), Inches(SLIDE_W - 0.6), Inches(0.35) ) fp = foot.text_frame.paragraphs[0] r = fp.add_run() r.text = ( "Layout adapted from Page MJ et al. PRISMA 2020 flow diagram, BMJ 2021;372:n71. " "Distributed under CC-BY 4.0." ) r.font.size = Pt(8) r.font.italic = True r.font.color.rgb = GREY out_path.parent.mkdir(parents=True, exist_ok=True) prs.save(out_path) print(f"✓ wrote {out_path}") def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("--variant", choices=["new", "updated"], default="new") ap.add_argument( "--include-other-sources", action="store_true", help="Include the 'other sources' identification column (v2 layout).", ) ap.add_argument("--out", type=Path, required=True) args = ap.parse_args() build_template(args.variant, args.include_other_sources, args.out) if __name__ == "__main__": main() -
build_strobe_template.py 14.3 KB
#!/usr/bin/env python3 """Build an editable STROBE participant flow diagram (.pptx) from a YAML config. Why this script exists ---------------------- The Graphviz path (``generate_flow_diagram.R --type strobe``) renders an auto-fitting monochrome diagram suitable for journal submission, but co-authors often want to nudge box positions, edit prose, or recolor stage labels in PowerPoint. This script produces a fully-editable .pptx in the classical Identification → Screening → Inclusion → Analysis convention used by the BMJ / Radiology / KJR cohort-study examples (Randolph 2018, PLoS ONE 2021). Unlike PRISMA, STROBE flow diagrams have study-specific spine structure (number of cohort-stage boxes and exclusions varies per study), so this is a single parametric builder rather than the PRISMA two-step (``build_prisma2020_template.py`` then ``fill_prisma_template.py``). YAML schema ----------- output_pptx: figures/figure1_strobe.pptx # required slide_size: [10.5, 11.0] # inches; auto-computed if omitted title: "Figure 1. STROBE participant flow diagram" # optional # `stages:` is OPTIONAL. STROBE itself does not mandate a phase column — # most cohort papers omit it (the column is a PRISMA 2020 convention). When # `stages:` is present, the script renders a left phase column and merges # consecutive same-stage spine rows under one label. When omitted (or empty), # the spine shifts left, the slide narrows, and the figure looks like the # plainer STROBE convention. stages: - {name: Identification, color: "#cfe1f5"} # color = stage box fill - {name: Screening, color: "#cfe1f5"} - {name: Inclusion, color: "#cfe1f5"} - {name: Analysis, color: "#cfe1f5"} spine: - {id: enrolled, stage: Identification, text: "..."} - {id: screened, stage: Screening, text: "..."} - {id: eligible, stage: Inclusion, text: "..."} - {id: primary, stage: Analysis, text: "..."} - {id: landmark, stage: Analysis, text: "..."} # consecutive same-stage rows share one stage label exclusions: - {after: enrolled, text: "Excluded (n = 1,200):\\n- did not meet the index finding on any screening"} - {after: screened, text: "Excluded (n = 3):\\n- prior outcome event / zero post-baseline follow-up"} Usage ----- python3 build_strobe_template.py \ --config figures/figure1_strobe.yaml \ --out figures/figure1_strobe.pptx Open the resulting .pptx in PowerPoint to fine-tune positions or styling before saving as PDF / TIFF for submission. """ from __future__ import annotations import argparse import json import sys from pathlib import Path from _strobe_cascade import check_cascade try: import yaml HAS_YAML = True except ImportError: HAS_YAML = False from lxml import etree from pptx import Presentation from pptx.dml.color import RGBColor from pptx.enum.shapes import MSO_SHAPE from pptx.enum.text import MSO_ANCHOR, PP_ALIGN from pptx.oxml.ns import qn from pptx.util import Inches, Pt # ── Defaults ──────────────────────────────────────────────────────────────── DEFAULT_SLIDE_W = 10.5 # default width when slide_size unset; height auto-computed FALLBACK_SLIDE_H = 11.0 # only used if auto-compute can't fit ABSOLUTE_MAX_SLIDE_H = 14.0 # PowerPoint hard upper bound for legible single-slide layouts # Column geometry (inches) — tighter than widescreen; better for vertical flow PHASE_X = 0.4 PHASE_W = 1.4 SPINE_X = 2.2 SPINE_W = 3.6 EXCL_X = 6.4 EXCL_W = 3.7 # Vertical spacing TITLE_Y = 0.25 TITLE_H = 0.5 TOP_PAD = 0.35 # below title before first row ROW_GAP = 0.35 # vertical gap between adjacent rows DEFAULT_ROW_H = 1.20 # slightly taller boxes for better text breathing room DEFAULT_EXCL_H = 1.05 BOTTOM_PAD = 0.4 # margin below last row # Colors NAVY = RGBColor(0x1F, 0x3A, 0x68) WHITE = RGBColor(0xFF, 0xFF, 0xFF) BLACK = RGBColor(0x00, 0x00, 0x00) LBLUE = RGBColor(0xCF, 0xE1, 0xF5) # ── Helpers ───────────────────────────────────────────────────────────────── def _parse_color(hex_str: str | None, default: RGBColor) -> RGBColor: if not hex_str: return default h = hex_str.lstrip("#") return RGBColor(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) def _readable_text_color(bg: RGBColor) -> RGBColor: """Return BLACK or WHITE depending on background luminance.""" # Relative luminance per WCAG (sRGB approx without gamma) r, g, b = bg[0], bg[1], bg[2] lum = 0.299 * r + 0.587 * g + 0.114 * b return RGBColor(0x1F, 0x3A, 0x68) if lum > 160 else WHITE def add_box(slide, x, y, w, h, text, *, fill=WHITE, border=BLACK, font_color=BLACK, font_size=11, bold_first=True, anchor_middle=True, align_center=True, line_pt=1.0, dash=False): shape = slide.shapes.add_shape( MSO_SHAPE.ROUNDED_RECTANGLE, Inches(x), Inches(y), Inches(w), Inches(h), ) shape.fill.solid() shape.fill.fore_color.rgb = fill shape.line.color.rgb = border shape.line.width = Pt(line_pt) if dash: ln = shape.line._get_or_add_ln() # Remove any existing prstDash to keep idempotent for existing in ln.findall(qn("a:prstDash")): ln.remove(existing) prst = etree.SubElement(ln, qn("a:prstDash")) prst.set("val", "dash") tf = shape.text_frame tf.word_wrap = True tf.margin_left = tf.margin_right = Inches(0.10) tf.margin_top = tf.margin_bottom = Inches(0.06) if anchor_middle: tf.vertical_anchor = MSO_ANCHOR.MIDDLE lines = str(text).split("\n") for i, line in enumerate(lines): para = tf.paragraphs[0] if i == 0 else tf.add_paragraph() para.alignment = PP_ALIGN.CENTER if align_center else PP_ALIGN.LEFT run = para.add_run() run.text = line run.font.size = Pt(font_size) run.font.color.rgb = font_color run.font.bold = bool(bold_first and i == 0) return shape def add_arrow(slide, x1, y1, x2, y2, *, color=BLACK, width_pt=1.25): line = slide.shapes.add_connector(1, Inches(x1), Inches(y1), Inches(x2), Inches(y2)) line.line.color.rgb = color line.line.width = Pt(width_pt) # Arrow head on the destination end line_xml = line.line._get_or_add_ln() tail = etree.SubElement(line_xml, qn("a:tailEnd")) tail.set("type", "triangle") tail.set("w", "med") tail.set("len", "med") def add_right_angle_arrow(slide, x1, y1, x2, y2, *, color=BLACK, width_pt=1.0): """Spine right edge → exclusion left edge with a horizontal stub then arrow.""" # Use a single straight horizontal connector since both ends share y add_arrow(slide, x1, y1, x2, y2, color=color, width_pt=width_pt) # ── Loader ────────────────────────────────────────────────────────────────── def load_config(path: Path) -> dict: text = path.read_text(encoding="utf-8") if path.suffix in (".yaml", ".yml"): if not HAS_YAML: sys.exit("PyYAML not installed; install or use JSON config.") return yaml.safe_load(text) if path.suffix == ".json": return json.loads(text) try: return json.loads(text) except json.JSONDecodeError: if HAS_YAML: return yaml.safe_load(text) sys.exit("Config not JSON and PyYAML unavailable.") # ── Builder ──────────────────────────────────────────────────────────────── def build(cfg: dict, out_path: Path) -> None: # Layout dimensions decided BEFORE slide creation so we can right-size the # canvas to the content (no large empty bottom strip). spine = cfg["spine"] n_spine = len(spine) spine_h = float(cfg.get("spine_box_height", DEFAULT_ROW_H)) excl_h = float(cfg.get("exclusion_box_height", DEFAULT_EXCL_H)) title_text = cfg.get("title") # Phase column is optional. STROBE itself does not mandate one (PRISMA does); # most cohort papers omit it. Render only when the user supplies a non-empty # `stages:` list in the YAML config. stages_cfg = cfg.get("stages") or [] has_phase_column = bool(stages_cfg) # Effective horizontal positions (collapse left margin when no phase column) if has_phase_column: spine_x_eff = SPINE_X excl_x_eff = EXCL_X else: excl_gap = EXCL_X - SPINE_X - SPINE_W # preserve the spine→excl gap spine_x_eff = PHASE_X excl_x_eff = spine_x_eff + SPINE_W + excl_gap avail_top = TITLE_Y + TITLE_H + TOP_PAD if title_text else TITLE_Y row_pitch = spine_h + ROW_GAP last_row_end = avail_top + (n_spine - 1) * row_pitch + spine_h auto_h = last_row_end + BOTTOM_PAD auto_w = excl_x_eff + EXCL_W + PHASE_X # right margin == left margin if "slide_size" in cfg: slide_w, slide_h = cfg["slide_size"] else: slide_w = max(auto_w, 4.0) slide_h = min(max(auto_h, 4.0), ABSOLUTE_MAX_SLIDE_H) # If the user gave a slide_size that's too short for the content, scale boxes down needed = last_row_end + BOTTOM_PAD if needed > slide_h: scale = (slide_h - avail_top - BOTTOM_PAD) / (n_spine * row_pitch - ROW_GAP) spine_h = max(0.7, spine_h * scale) excl_h = max(0.6, excl_h * scale) row_pitch = spine_h + ROW_GAP prs = Presentation() prs.slide_width = Inches(slide_w) prs.slide_height = Inches(slide_h) blank_layout = prs.slide_layouts[6] slide = prs.slides.add_slide(blank_layout) # Title if title_text: title_box = slide.shapes.add_textbox( Inches(PHASE_X), Inches(TITLE_Y), Inches(slide_w - PHASE_X * 2), Inches(TITLE_H), ) tf = title_box.text_frame p = tf.paragraphs[0] run = p.add_run() run.text = title_text run.font.size = Pt(13) run.font.bold = True run.font.color.rgb = NAVY spine_y = {} for i, b in enumerate(spine): spine_y[b["id"]] = avail_top + i * row_pitch # Phase column (optional) — group consecutive same-stage rows under one merged label. if has_phase_column: stage_color_lookup = {s["name"]: _parse_color(s.get("color"), NAVY) for s in stages_cfg} stages_seq = [b.get("stage") for b in spine] i = 0 while i < n_spine: j = i while j + 1 < n_spine and stages_seq[j + 1] == stages_seq[i]: j += 1 sname = stages_seq[i] y_top = spine_y[spine[i]["id"]] y_bot = spine_y[spine[j]["id"]] + spine_h h = y_bot - y_top stage_fill = stage_color_lookup.get(sname, NAVY) add_box( slide, PHASE_X, y_top, PHASE_W, h, sname, fill=stage_fill, border=stage_fill, font_color=_readable_text_color(stage_fill), font_size=14, bold_first=True, line_pt=0.0, ) i = j + 1 # Spine boxes + arrows for k, b in enumerate(spine): y = spine_y[b["id"]] add_box( slide, spine_x_eff, y, SPINE_W, spine_h, b["text"], fill=WHITE, border=BLACK, font_color=BLACK, font_size=11, bold_first=True, ) if k > 0: prev_y = spine_y[spine[k - 1]["id"]] add_arrow( slide, spine_x_eff + SPINE_W / 2, prev_y + spine_h, spine_x_eff + SPINE_W / 2, y, ) # Exclusion boxes + connector arrows # Vertically center each exclusion on its spine row's mid-height so the # connector arrow can be strictly horizontal (no diagonal segments). for excl in cfg.get("exclusions", []): after_id = excl["after"] y_after = spine_y[after_id] y_mid = y_after + spine_h / 2 y_excl_top = y_mid - excl_h / 2 add_box( slide, excl_x_eff, y_excl_top, EXCL_W, excl_h, excl["text"], fill=WHITE, border=BLACK, font_color=BLACK, font_size=10, bold_first=False, align_center=False, # left-align text in exclusions dash=True, # dashed border to visually distinguish exclusion side-branches ) # Strictly horizontal connector: both endpoints at y_mid add_arrow( slide, spine_x_eff + SPINE_W, y_mid, excl_x_eff, y_mid, ) out_path.parent.mkdir(parents=True, exist_ok=True) prs.save(out_path) # ── CLI ───────────────────────────────────────────────────────────────────── def main(): p = argparse.ArgumentParser(description="Build an editable STROBE flow diagram .pptx from a YAML config.") p.add_argument("--config", required=True, type=Path, help="YAML/JSON STROBE config") p.add_argument("--out", required=True, type=Path, help="Output .pptx path") p.add_argument("--strict-cascade", action="store_true", help="refuse to build if the exclusion cascade does not close " "(A - sum(exclusions after A) != next box)") args = p.parse_args() cfg = load_config(args.config) if not isinstance(cfg, dict): sys.exit(f"Config root must be a mapping; got {type(cfg)}") # The numbers a reviewer sees are in the figure, not the prose — assert the cascade closes # before rendering. Warn loudly by default; refuse under --strict-cascade. cascade = check_cascade(cfg) for f in cascade: sys.stderr.write(f"⚠️ {f['detail']}\n") if cascade and args.strict_cascade: sys.exit("STROBE cascade does not close; refusing to build (drop --strict-cascade " "to build anyway).") build(cfg, args.out) print(f"Wrote {args.out}") if __name__ == "__main__": main() -
critic_figure.py 9.3 KB
#!/usr/bin/env python3 """critic_figure.py — Quantitative checks for make-figures skill output. Part of the medsci-skills Critic Loop (Phase 1). This script runs deterministic checks that do NOT require a language model: - DPI and physical dimensions vs. journal specification - Dominant-color analysis against the Wong colorblind-safe palette - OCR text extraction for minimum font-size estimation and optional coverage comparison against a source-text file The qualitative side of the Critic Loop (layout balance, hierarchy, readability, exemplar comparison) is handled by the Claude session afterward using the rubrics in references/critic_rubrics/. Usage: python critic_figure.py figures/fig1_stard.png \ --type stard --spec-min-dpi 600 --spec-width-in 7.0 \ --source-text figures/fig1_stard.txt \ --out figures/fig1_stard.critique.json """ from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Optional from PIL import Image WONG_PALETTE: dict[str, tuple[int, int, int]] = { "black": (0, 0, 0), "orange": (230, 159, 0), "sky_blue": (86, 180, 233), "bluish_green": (0, 158, 115), "yellow": (240, 228, 66), "blue": (0, 114, 178), "vermillion": (213, 94, 0), "reddish_purple": (204, 121, 167), } NEUTRAL_TOLERANCE = 25 PALETTE_DISTANCE_THRESHOLD = 60 DOMINANT_COLOR_TOP_N = 12 DOMINANT_COLOR_MIN_FRACTION = 0.002 OUT_OF_PALETTE_TOLERANCE = 0.15 MIN_READABLE_PX = 14 def color_distance(c1, c2) -> float: return sum((a - b) ** 2 for a, b in zip(c1, c2)) ** 0.5 def classify_color(rgb) -> str: r, g, b = rgb if max(r, g, b) - min(r, g, b) < NEUTRAL_TOLERANCE: return "neutral" best_name, best_dist = None, float("inf") for name, palette_rgb in WONG_PALETTE.items(): d = color_distance(rgb, palette_rgb) if d < best_dist: best_name, best_dist = name, d if best_dist < PALETTE_DISTANCE_THRESHOLD: return f"wong:{best_name}" return "out_of_palette" def check_palette(img: Image.Image) -> dict: rgb_img = img.convert("RGB") total = rgb_img.size[0] * rgb_img.size[1] quant = rgb_img.quantize(colors=64).convert("RGB") pixels = quant.getcolors(maxcolors=64 * 64) if not pixels: return {"passed": None, "note": "too many colors to quantize"} dominant = [] out_frac = 0.0 for count, rgb in sorted(pixels, key=lambda x: -x[0])[:DOMINANT_COLOR_TOP_N]: fraction = count / total if fraction < DOMINANT_COLOR_MIN_FRACTION: continue label = classify_color(rgb) dominant.append({"rgb": list(rgb), "fraction": round(fraction, 4), "class": label}) if label == "out_of_palette": out_frac += fraction return { "passed": out_frac < OUT_OF_PALETTE_TOLERANCE, "out_of_palette_fraction": round(out_frac, 4), "tolerance": OUT_OF_PALETTE_TOLERANCE, "dominant_colors": dominant, } def check_dimensions(img: Image.Image, spec: dict) -> dict: w_px, h_px = img.size dpi = img.info.get("dpi", (None, None)) dpi_x = dpi[0] if dpi and dpi[0] else None dpi_y = dpi[1] if dpi and dpi[1] else None result = { "width_px": w_px, "height_px": h_px, "dpi_x": dpi_x, "dpi_y": dpi_y, } min_dpi = spec.get("min_dpi") width_in = spec.get("width_in") if min_dpi and dpi_x: result["dpi_meets_spec"] = dpi_x >= min_dpi result["required_dpi"] = min_dpi if width_in and dpi_x: actual_width_in = w_px / dpi_x result["width_in"] = round(actual_width_in, 2) result["width_matches_spec"] = abs(actual_width_in - width_in) < 0.3 result["required_width_in"] = width_in return result def check_ocr(image_path: Path, source_text: Optional[str] = None) -> dict: try: import pytesseract except ImportError: return {"passed": None, "note": "pytesseract not installed; install with `pip install pytesseract` (tesseract binary already at /opt/homebrew/bin/tesseract)"} try: data = pytesseract.image_to_data( str(image_path), output_type=pytesseract.Output.DICT ) except Exception as exc: return {"passed": None, "note": f"tesseract failed: {exc}"} words = [] heights = [] for i, word in enumerate(data.get("text", [])): if not word.strip(): continue conf_raw = data["conf"][i] try: conf = int(conf_raw) except (TypeError, ValueError): conf = -1 if conf < 50: continue words.append(word.strip()) heights.append(data["height"][i]) result: dict = { "word_count": len(words), } if heights: heights_sorted = sorted(heights) result["median_word_height_px"] = heights_sorted[len(heights_sorted) // 2] result["min_word_height_px"] = min(heights) result["min_readable_threshold_px"] = MIN_READABLE_PX if source_text: source_words = {w.lower().strip(".,:;()[]") for w in source_text.split() if len(w) > 2} ocr_words = {w.lower().strip(".,:;()[]") for w in words if len(w) > 2} missing = sorted(source_words - ocr_words) result["missing_source_words"] = missing[:20] result["missing_source_word_count"] = len(missing) result["source_word_coverage"] = round( len(source_words & ocr_words) / max(1, len(source_words)), 2 ) return result def critique( image_path: Path, figure_type: Optional[str] = None, journal: Optional[str] = None, source_text: Optional[str] = None, spec: Optional[dict] = None, ) -> dict: img = Image.open(image_path) spec = spec or {} checks = { "dimensions": check_dimensions(img, spec), "palette": check_palette(img), "ocr": check_ocr(image_path, source_text=source_text), } flags = [] dim = checks["dimensions"] if dim.get("dpi_meets_spec") is False: flags.append( f"DPI below journal spec ({dim.get('dpi_x')} < {dim.get('required_dpi')})" ) if dim.get("width_matches_spec") is False: flags.append( f"Width deviates from journal spec ({dim.get('width_in')} in vs {dim.get('required_width_in')} in)" ) pal = checks["palette"] if pal.get("passed") is False: flags.append( f"{pal['out_of_palette_fraction'] * 100:.1f}% pixels outside Wong palette (tolerance {OUT_OF_PALETTE_TOLERANCE * 100:.0f}%)" ) ocr = checks["ocr"] min_h = ocr.get("min_word_height_px") if min_h and min_h < MIN_READABLE_PX: flags.append(f"Smallest OCR text only {min_h}px tall (<{MIN_READABLE_PX}px threshold)") if ocr.get("missing_source_word_count") and ocr["missing_source_word_count"] > 3: flags.append( f"{ocr['missing_source_word_count']} source words not detected by OCR (possible cropping/truncation)" ) return { "image": str(image_path), "figure_type": figure_type, "journal": journal, "checks": checks, "flags": flags, "summary": "PASS" if not flags else f"{len(flags)} issue(s) flagged", } def main() -> int: p = argparse.ArgumentParser(description="Quantitative figure critic (medsci-skills Critic Loop, Phase 1).") p.add_argument("image", help="Path to generated figure (PNG)") p.add_argument( "--type", dest="figure_type", choices=[ "stard", "consort", "prisma", "pipeline", "roc", "forest", "km", "calibration", "bland_altman", "confusion_matrix", "visual_abstract", "other", ], ) p.add_argument("--journal", help="Journal key (e.g., radiology, radiology_ai, eur_radiol)") p.add_argument("--source-text", help="Path to text file with expected source strings for OCR coverage check") p.add_argument("--spec-width-in", type=float, help="Expected figure width in inches (per figure_specs.md)") p.add_argument("--spec-min-dpi", type=int, help="Minimum DPI (600 for line art, 300 for halftone)") p.add_argument("--out", default=None, help="Path to JSON report (default: {image}.critique.json)") args = p.parse_args() image_path = Path(args.image) if not image_path.exists(): print(f"ERROR: {image_path} not found", file=sys.stderr) return 2 source_text = None if args.source_text: source_path = Path(args.source_text) if not source_path.exists(): print(f"WARN: source text {source_path} not found; skipping OCR coverage check", file=sys.stderr) else: source_text = source_path.read_text(encoding="utf-8", errors="ignore") report = critique( image_path, figure_type=args.figure_type, journal=args.journal, source_text=source_text, spec={"width_in": args.spec_width_in, "min_dpi": args.spec_min_dpi}, ) out_path = Path(args.out) if args.out else image_path.with_suffix(image_path.suffix + ".critique.json") out_path.write_text(json.dumps(report, indent=2, ensure_ascii=False)) print(json.dumps({"summary": report["summary"], "flags": report["flags"]}, indent=2, ensure_ascii=False)) print(f"\nFull report: {out_path}", file=sys.stderr) return 0 if not report["flags"] else 1 if __name__ == "__main__": sys.exit(main()) -
derive_figure_legend_counts.py 5.4 KB
#!/usr/bin/env python3 """Reconcile a flow-diagram figure caption against the flow-diagram SSOT. A recurring submission error: the Figure 1 caption states participant counts ("n = 1,284 assessed ... n = 998 in the analytic cohort") that disagree with the boxes of the flow diagram itself. It happens because the flow-diagram config (the SSOT, e.g. `figure1_strobe_graphviz.yaml` consumed by generate_flow_diagram.R) is updated for a final cohort lock while the hand-written caption is not. This script re-derives the counts from the flow config and flags any `n = N` in the caption that the diagram does not contain. INPUTS --flow-config the flow-diagram config file (YAML / R / text) whose box labels carry the counts. Parsed as raw text (regex), so no YAML dependency and it works for any flow-tool config. --manuscript manuscript markdown (the Figure 1 caption is located by header) OR --caption to pass the caption text directly. OUTPUT (--out path) {flow_counts, caption_counts, stale_in_caption, missing_in_caption, verdict} `stale_in_caption` (a caption count absent from the flow SSOT) is the Major finding. Exit 1 (with --strict) when any stale count exists. Stdlib-only (re / json / argparse). Exit codes: 0 clean (or report-only), 1 caption count not in the flow SSOT (with --strict), 2 input/usage error. """ from __future__ import annotations import argparse import json import re import sys from pathlib import Path # "n = 1,284", "n=998", "N = 1284" — the standard flow-box / caption notation. N_RE = re.compile(r"\bn\s*=\s*([0-9][0-9,]*)", re.I) CAPTION_HDR_RE = re.compile( r"^#{0,4}\s*\**\s*(Figure\s*1|Fig\.?\s*1|Figure\s+1\.)\b", re.I | re.M ) def _ints(text: str) -> list[int]: out = [] for m in N_RE.finditer(text): try: out.append(int(m.group(1).replace(",", ""))) except ValueError: pass return out def extract_caption(manuscript_text: str) -> str: """Return the Figure 1 caption block (header line to the next blank line / header).""" m = CAPTION_HDR_RE.search(manuscript_text) if not m: return "" start = m.start() rest = manuscript_text[m.end():] # caption ends at the next blank line followed by a header, or a new "Figure N" end_rel = len(rest) nxt = re.search(r"\n\s*\n|^#{1,4}\s|\bFigure\s*[2-9]\b", rest, re.M) if nxt: end_rel = nxt.start() return manuscript_text[start:m.end() + end_rel] def main() -> int: ap = argparse.ArgumentParser(description="Reconcile Figure 1 caption counts against the flow SSOT.") ap.add_argument("--flow-config", required=True, help="flow-diagram config (YAML/R/text)") ap.add_argument("--manuscript", help="manuscript markdown (Figure 1 caption auto-located)") ap.add_argument("--caption", help="caption text directly (alternative to --manuscript)") ap.add_argument("--out", help="write JSON artifact to this path") ap.add_argument("--strict", action="store_true", help="exit 1 if a caption count is not in the SSOT") args = ap.parse_args() fc = Path(args.flow_config) if not fc.is_file(): sys.stderr.write(f"ERROR: flow config not found: {args.flow_config}\n") return 2 flow_counts = sorted(set(_ints(fc.read_text(encoding="utf-8")))) if args.caption: caption = args.caption elif args.manuscript: mp = Path(args.manuscript) if not mp.is_file(): sys.stderr.write(f"ERROR: manuscript not found: {args.manuscript}\n") return 2 caption = extract_caption(mp.read_text(encoding="utf-8")) if not caption: sys.stderr.write("WARN: no 'Figure 1' caption located; pass --caption to override.\n") else: sys.stderr.write("ERROR: provide --manuscript or --caption\n") return 2 caption_counts = sorted(set(_ints(caption))) flow_set = set(flow_counts) stale = [n for n in caption_counts if n not in flow_set] # caption cites a number the diagram lacks missing = [n for n in flow_counts if n not in set(caption_counts)] # diagram box not mentioned in caption result = { "flow_config": str(fc), "flow_counts": flow_counts, "caption_counts": caption_counts, "stale_in_caption": stale, "missing_in_caption": missing, "verdict": "MISMATCH" if stale else "OK", "suggested_fix": ( "Re-derive the caption counts from the flow-diagram config and update the caption; " "the flow diagram is the single source of truth." ) if stale else None, } print("=" * 41) print(" Figure 1 caption ↔ flow SSOT reconciliation") print("=" * 41) print(f"flow counts (SSOT): {flow_counts}") print(f"caption counts: {caption_counts}") if stale: print(f"\nMISMATCH: caption cites {stale} not present in the flow diagram.") print(result["suggested_fix"]) else: print("\nOK: every caption count is present in the flow diagram.") if missing: print(f"(note: flow boxes not mentioned in the caption: {missing})") if args.out: Path(args.out).parent.mkdir(parents=True, exist_ok=True) Path(args.out).write_text(json.dumps({"detector": "derive_figure_legend_counts", **result}, indent=2), encoding="utf-8") print(f"wrote {args.out}") return 1 if (args.strict and stale) else 0 if __name__ == "__main__": sys.exit(main()) -
export_portal_tiff.py 7.5 KB
#!/usr/bin/env python3 """Export a figure to a portal-ready TIFF — LZW-compressed, RGBA→RGB white-flattened. Why this exists. Two submission-portal facts collide on figure upload: 1. Some portals accept only a fixed raster set and NOT PNG. Springer Nature's SNAPP, for one, takes `.jpeg` / `.tiff` / `.eps` — a PNG has to be converted on the spot. 2. A portal caps figure size (JACC: Asia rejects a figure over 25 MB). A raw, uncompressed 600-dpi RGBA TIFF blows straight past that; the *same* image saved LZW-compressed with the alpha channel flattened away is a fraction of the size and pixel-identical. The naive conversion also introduces a print defect: a TIFF that keeps an alpha channel renders the transparent regions BLACK on many print/production pipelines. Flattening the alpha onto a white background (the paper) is what a human does by hand in Photoshop; this does it deterministically and then PROVES the result is pixel-identical to that flatten before it hands you the file. What it does: * opens the input raster (any PIL-readable format — PNG, TIFF, BMP, …); * if it carries alpha/transparency (RGBA / LA / palette-with-transparency), composites it onto a solid background (white by default) to get RGB; a plain RGB/L image is kept; * saves TIFF with LZW compression (lossless), preserving dpi; * VERIFIES the output by independently re-flattening the source and comparing bytes — refuses (exit 1) if the produced TIFF is not pixel-identical to the expected flatten; * reports the before/after byte size and, with --max-mb, refuses an output that still exceeds the portal cap (so the failure surfaces here, not at the upload button). This is a figure PRODUCER (like render_core_figures.py), not a manuscript detector. It requires Pillow — the same runtime dependency every raster figure helper in this skill already has — and does nothing over the network. Usage: export_portal_tiff.py --in figure.png # -> figure.tiff (white bg, LZW) export_portal_tiff.py --in fig.png --out fig.tiff --max-mb 25 export_portal_tiff.py --in fig.png --background 255,255,255 --dpi 600 Exit codes: 0 success (verified), 1 verification mismatch / over the --max-mb cap, 2 input/usage error (missing file, unreadable image, Pillow absent). """ from __future__ import annotations import argparse import sys from pathlib import Path # A palette image can carry per-pixel transparency via a `transparency` info key. ALPHA_MODES = ("RGBA", "LA", "PA", "La") def _load_pillow(): try: from PIL import Image # noqa: F401 except ImportError: sys.stderr.write( "ERROR: Pillow is required (pip install Pillow) — the same dependency the " "other raster figure helpers in /make-figures use.\n") sys.exit(2) from PIL import Image return Image def _has_alpha(img) -> bool: """True if the image carries per-pixel transparency that a flatten must resolve.""" if img.mode in ALPHA_MODES: return True # A palette (P) or grayscale image can still declare a transparent colour. return "transparency" in img.info def flatten_to_rgb(img, background): """Return an RGB copy with any alpha composited onto `background` (an (R,G,B) tuple). A plain RGB/grayscale image (no transparency) is converted straight to RGB. This is the single definition of the flatten, used both to produce the output and to verify it.""" if not _has_alpha(img): return img.convert("RGB") rgba = img.convert("RGBA") Image = _load_pillow() bg = Image.new("RGB", rgba.size, background) bg.paste(rgba, mask=rgba.split()[-1]) # last band is alpha return bg def _parse_bg(s: str): parts = s.split(",") if len(parts) != 3: raise argparse.ArgumentTypeError("background must be R,G,B (e.g. 255,255,255)") try: vals = tuple(int(p) for p in parts) except ValueError: raise argparse.ArgumentTypeError("background components must be integers 0-255") if not all(0 <= v <= 255 for v in vals): raise argparse.ArgumentTypeError("background components must be 0-255") return vals def export(in_path: Path, out_path: Path, background, dpi, max_mb): Image = _load_pillow() try: src = Image.open(in_path) src.load() except Exception as e: # noqa: BLE001 — PIL raises a variety of decode errors sys.stderr.write(f"ERROR: could not open image {in_path}: {e}\n") sys.exit(2) expected = flatten_to_rgb(src, background) save_kwargs = {"format": "TIFF", "compression": "tiff_lzw"} src_dpi = dpi or src.info.get("dpi") if src_dpi: save_kwargs["dpi"] = tuple(src_dpi) if not isinstance(src_dpi, (int, float)) else (src_dpi, src_dpi) out_path.parent.mkdir(parents=True, exist_ok=True) expected.save(out_path, **save_kwargs) # Verify: reopen the produced TIFF and require it to be pixel-identical to the flatten, # LZW-compressed, and alpha-free. A silent decode surprise fails here, not at upload. with Image.open(out_path) as produced: produced.load() compression = produced.tag_v2.get(259) if hasattr(produced, "tag_v2") else None prod_rgb = produced.convert("RGB") identical = prod_rgb.tobytes() == expected.tobytes() problems = [] if compression != 5: # TIFF Compression tag: 5 == LZW problems.append(f"output is not LZW-compressed (Compression tag={compression}, expected 5)") if not identical: problems.append("output TIFF is NOT pixel-identical to the white-flattened source") in_mb = in_path.stat().st_size / (1024 * 1024) out_mb = out_path.stat().st_size / (1024 * 1024) if max_mb is not None and out_mb > max_mb: problems.append(f"output is {out_mb:.1f} MB, over the --max-mb {max_mb} portal cap") print("=" * 52) print(" Portal TIFF export") print("=" * 52) print(f" in: {in_path} ({in_mb:.2f} MB, mode {src.mode})") print(f" out: {out_path} ({out_mb:.2f} MB, mode RGB, LZW)") print(f" flattened: {'alpha composited onto ' + str(background) if _has_alpha(src) else 'no alpha (RGB kept)'}") print(f" pixel-check: {'identical to source flatten' if identical else 'MISMATCH'}") if problems: print("\nFAIL: " + "; ".join(problems)) return 1 print("\nOK: portal-ready TIFF (LZW, RGB, pixel-identical to source).") return 0 def main() -> int: ap = argparse.ArgumentParser( description="Export a figure to a portal-ready TIFF (LZW, RGBA→RGB white-flatten).") ap.add_argument("--in", dest="inp", required=True, help="input raster image (PNG/TIFF/…)") ap.add_argument("--out", default=None, help="output .tiff (default: input stem + .tiff)") ap.add_argument("--background", type=_parse_bg, default=(255, 255, 255), help="RGB fill for transparent regions (default 255,255,255 = white)") ap.add_argument("--dpi", type=int, default=None, help="override dpi (default: keep source dpi)") ap.add_argument("--max-mb", type=float, default=None, help="refuse (exit 1) if the output still exceeds this many MB (e.g. 25 for a portal cap)") args = ap.parse_args() in_path = Path(args.inp) if not in_path.is_file(): sys.stderr.write(f"ERROR: input not found: {in_path}\n") return 2 out_path = Path(args.out) if args.out else in_path.with_suffix(".tiff") return export(in_path, out_path, args.background, args.dpi, args.max_mb) if __name__ == "__main__": sys.exit(main()) -
extract_exemplar_from_pdf.py 6.8 KB
#!/usr/bin/env python3 r"""extract_exemplar_from_pdf.py — Extract a figure region from a PDF page to build the exemplar reference set used by the make-figures Critic Loop. Renders a single PDF page at 300 DPI (configurable), optionally crops a sub-region, and writes the PNG + a YAML metadata sidecar + a _why.md stub into references/exemplar_diagrams/{type}/. Example: python extract_exemplar_from_pdf.py \ --pdf ~/Zotero/storage/ABCD/Yan2017.pdf \ --page 3 \ --type stard \ --label Yan2017_STARD \ --doi 10.1148/radiol.2017170371 \ --crop 0.05,0.1,0.95,0.6 \ --dpi 300 """ from __future__ import annotations import argparse import sys from pathlib import Path from typing import Optional import fitz # PyMuPDF DEFAULT_EXEMPLAR_DIR = Path(__file__).resolve().parent.parent / "references" / "exemplar_diagrams" VALID_TYPES = {"stard", "consort", "prisma", "pipeline", "roc", "forest", "km", "other"} def render_page(pdf_path: Path, page_num: int, dpi: int, crop: Optional[tuple] = None) -> bytes: doc = fitz.open(pdf_path) try: if page_num < 1 or page_num > doc.page_count: raise ValueError(f"Page {page_num} out of range (1-{doc.page_count})") page = doc[page_num - 1] zoom = dpi / 72.0 matrix = fitz.Matrix(zoom, zoom) if crop: rect = page.rect x0, y0, x1, y1 = crop clip = fitz.Rect( rect.x0 + rect.width * x0, rect.y0 + rect.height * y0, rect.x0 + rect.width * x1, rect.y0 + rect.height * y1, ) pix = page.get_pixmap(matrix=matrix, clip=clip, alpha=False) else: pix = page.get_pixmap(matrix=matrix, alpha=False) return pix.tobytes("png") finally: doc.close() def parse_crop(s: str) -> tuple: parts = [float(p.strip()) for p in s.split(",")] if len(parts) != 4: raise argparse.ArgumentTypeError("crop must be 'x0,y0,x1,y1' as fractions 0.0–1.0") for v in parts: if not 0.0 <= v <= 1.0: raise argparse.ArgumentTypeError(f"crop fraction {v} out of range 0.0–1.0") if parts[0] >= parts[2] or parts[1] >= parts[3]: raise argparse.ArgumentTypeError("crop x0<x1 and y0<y1 required") return tuple(parts) def write_metadata(meta_path: Path, **fields) -> None: lines = ["# Exemplar metadata (YAML)"] for k, v in fields.items(): if v is None: continue if isinstance(v, str): lines.append(f'{k}: "{v}"') elif isinstance(v, (list, tuple)): lines.append(f"{k}: {list(v)}") else: lines.append(f"{k}: {v}") meta_path.write_text("\n".join(lines) + "\n", encoding="utf-8") def write_why_stub(why_path: Path, label: str, figure_type: str) -> None: if why_path.exists(): return # don't clobber existing notes why_path.write_text( f"""# Why this exemplar is good — {label} ({figure_type}) <!-- 50–100 words on why this figure is a quality anchor. Fill in during curation. --> Hierarchy / structure: Whitespace & balance: Typography (font size, weight, alignment): Emphasis (which elements are visually strongest, why): Color usage: Weaknesses (if any — nothing is perfect): """, encoding="utf-8", ) def extract_one( pdf: Path, page: int, figure_type: str, label: str, doi: str, license_: str, crop: Optional[tuple], dpi: int, exemplar_dir: Path, ) -> Path: if figure_type not in VALID_TYPES: raise ValueError(f"type must be one of {sorted(VALID_TYPES)}, got {figure_type}") target_dir = exemplar_dir / figure_type target_dir.mkdir(parents=True, exist_ok=True) png_bytes = render_page(pdf, page, dpi, crop) safe_label = label.replace("/", "_").replace(" ", "_") png_path = target_dir / f"{safe_label}.png" meta_path = target_dir / f"{safe_label}.meta.yaml" why_path = target_dir / f"{safe_label}_why.md" png_path.write_bytes(png_bytes) write_metadata( meta_path, label=label, figure_type=figure_type, source_pdf=str(pdf), page=page, crop=list(crop) if crop else None, dpi=dpi, doi=doi, license=license_, ) write_why_stub(why_path, label, figure_type) return png_path def main() -> int: p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) p.add_argument("--pdf", required=True, type=Path, help="Path to source PDF") p.add_argument("--page", required=True, type=int, help="1-based page number") p.add_argument("--type", dest="figure_type", required=True, choices=sorted(VALID_TYPES), help="Exemplar category (subdirectory under exemplar_diagrams/)") p.add_argument("--label", required=True, help="Short label, used as filename stem (e.g., Yan2017_STARD)") # --doi used to be optional, and that is exactly how ten figures ended up in a public, # MIT-licensed package with no record of whose they were. A tool that CAN produce an # unattributable exemplar eventually will. p.add_argument("--doi", required=True, help="DOI of the source paper. Required — no exception.") p.add_argument( "--license", required=True, dest="license_", help="The licence of the FIGURE (not the paper): CC-BY-4.0, CC0, public-domain, own-work. " "If you do not know it, you do not have permission to ship it — keep the exemplar " "local and uncommitted instead.", ) p.add_argument("--crop", default=None, type=parse_crop, help="Optional crop as 'x0,y0,x1,y1' fractions 0.0–1.0") p.add_argument("--dpi", type=int, default=300, help="Render DPI (default: 300)") p.add_argument("--exemplar-dir", type=Path, default=DEFAULT_EXEMPLAR_DIR, help="Base exemplar directory (default: references/exemplar_diagrams/)") args = p.parse_args() if not args.pdf.exists(): print(f"ERROR: PDF not found at {args.pdf}", file=sys.stderr) return 2 try: out = extract_one( pdf=args.pdf, page=args.page, figure_type=args.figure_type, label=args.label, doi=args.doi, license_=args.license_, crop=args.crop, dpi=args.dpi, exemplar_dir=args.exemplar_dir, ) except Exception as exc: print(f"ERROR: {exc}", file=sys.stderr) return 1 print(f"Extracted: {out}") print(f"Metadata: {out.with_suffix('')}.meta.yaml") print(f"Why stub: {out.with_name(out.stem + '_why.md')}") print("\nNext: open the _why.md and add a 50–100 word note on why this figure is a quality anchor.") return 0 if __name__ == "__main__": sys.exit(main()) -
fetch_official_templates.sh 4.2 KB
#!/usr/bin/env bash # Fetch official reporting guideline flow diagram / figure templates from # the canonical statement sites (PRISMA, CONSORT, STARD, SPIRIT). # # Idempotent: skips downloads when target already exists with non-zero size. # Network failures are non-fatal; the script reports per-target status at the end. # # Usage: # bash scripts/fetch_official_templates.sh # fetch all # bash scripts/fetch_official_templates.sh prisma2020 # one target # FORCE=1 bash scripts/fetch_official_templates.sh # re-download set -uo pipefail SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" DEST_ROOT="${SKILL_DIR}/templates/official" mkdir -p "${DEST_ROOT}" UA='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Claude-Code-medsci-skills/1.0' # target_id|filename|url # URLs verified 2026-04-27. PRISMA 2020 from prismastatement.org/s/. # CONSORT/SPIRIT migrated to consort-spirit.org (formerly consort-statement.org + # spirit-statement.org). STARD provides PDF only — checklist .docx fetched as # the closest official Word artifact; flow diagram remains PDF. TARGETS=( # PRISMA 2020 — both new-SR and updated-SR, both v1 (databases/registers only) # and v2 (incl. other sources). Official .docx, CC-BY 4.0. "prisma2020|PRISMA_2020_flow_new_SR_v1.docx|https://prismastatement.org/s/PRISMA_2020_flow_diagram_new_SRs_v1-lml8.docx" "prisma2020|PRISMA_2020_flow_new_SR_v2.docx|https://prismastatement.org/s/PRISMA_2020_flow_diagram_new_SRs_v2-t3jp.docx" "prisma2020|PRISMA_2020_flow_updated_SR_v1.docx|https://prismastatement.org/s/PRISMA_2020_flow_diagram_updated_SRs_v1-f8ly.docx" "prisma2020|PRISMA_2020_flow_updated_SR_v2.docx|https://prismastatement.org/s/PRISMA_2020_flow_diagram_updated_SRs_v2-dbrh.docx" # CONSORT 2025 (supersedes 2010) — main flow diagram + editable checklist "consort2010|CONSORT_2025_flow_diagram.docx|https://www.consort-spirit.org/_files/ugd/b5740e_019fabb52c9a4894abb681afcbff41f8.docx?dn=CONSORT_2025_flow_diagram.docx" "consort2010|CONSORT_2025_editable_checklist.docx|https://www.consort-spirit.org/_files/ugd/b628c8_6fa59b2c6db04efb9a9d0bc967a9fcf7.docx?dn=CONSORT%20%202025%20editable%20checklist.docx" # STARD 2015 — flow diagram is published as PDF only (no official Word source). # Ship the checklist .docx + the flow PDF as the canonical bundle. "stard2015|STARD_2015_flow_diagram.pdf|https://www.equator-network.org/wp-content/uploads/2015/03/STARD-2015-flow-diagram.pdf" "stard2015|STARD_2015_checklist.docx|https://www.equator-network.org/wp-content/uploads/2015/10/STARD-2015-Checklist.docx" # SPIRIT 2025 (supersedes 2013) — participant timeline + editable checklist "spirit2013|SPIRIT_2025_participant_timeline.docx|https://www.consort-spirit.org/_files/ugd/b5740e_5fad4e7a16bf44f6a2a0a809ad2a2e9d.docx?dn=SPIRIT%202025%20participant%20timeline.docx" "spirit2013|SPIRIT_2025_editable_checklist.docx|https://www.consort-spirit.org/_files/ugd/b5740e_667c45b02102408ab983c9704525597b.docx?dn=SPIRIT%202025%20editable%20checklist.docx" ) WANT="${1:-all}" FORCE="${FORCE:-0}" declare -a ok_list fail_list skip_list for entry in "${TARGETS[@]}"; do IFS='|' read -r tid fname url <<<"${entry}" if [[ "${WANT}" != "all" && "${WANT}" != "${tid}" ]]; then continue fi out="${DEST_ROOT}/${tid}/${fname}" if [[ "${FORCE}" != "1" && -s "${out}" ]]; then skip_list+=("${tid}/${fname}") continue fi echo "→ ${tid}/${fname}" if curl -fsSL --retry 2 --max-time 30 -A "${UA}" -o "${out}.tmp" "${url}"; then if [[ -s "${out}.tmp" ]]; then mv "${out}.tmp" "${out}" ok_list+=("${tid}/${fname}") else rm -f "${out}.tmp" fail_list+=("${tid}/${fname} (empty)") fi else rm -f "${out}.tmp" fail_list+=("${tid}/${fname} (HTTP)") fi done echo echo "── fetch summary ─────────────────────────────" printf 'OK : %s\n' "${ok_list[@]:-(none)}" printf 'SKIP : %s\n' "${skip_list[@]:-(none)}" printf 'FAIL : %s\n' "${fail_list[@]:-(none)}" if [[ ${#fail_list[@]} -gt 0 ]]; then echo echo "Some downloads failed. Statement sites occasionally rotate URLs;" echo "verify at: https://www.equator-network.org/reporting-guidelines/" exit 2 fi -
fill_prisma_template.py 4.7 KB
#!/usr/bin/env python3 """Populate count placeholders in a PRISMA 2020 .pptx template. Reads a template built by ``build_prisma2020_template.py`` (or any .pptx whose text frames contain ``{key}``-style tokens) and substitutes integer counts. Two input modes --------------- 1. **Positional CSV** — ``--counts "315,122,186,7,111,204,102,84,3,15"`` maps to a fixed sequence of keys appropriate for the most common DTA / IR meta-analysis workflow: n_db, n_dup, n_screened, n_screen_excluded, n_sought, n_assessed, n_excl_r1, n_excl_r2, n_excl_r3, n_studies This is a convenience for quick CLI use; remaining placeholders fall back to ``-`` so the figure remains visually balanced. 2. **JSON / YAML** — ``--counts-file counts.json`` provides a full mapping of every placeholder used in the template. Any keys not supplied render as ``-``. Usage ----- python3 fill_prisma_template.py \ --template templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \ --counts "315,122,186,7,111,204,102,84,3,15" \ --out fig1_prisma_filled.pptx python3 fill_prisma_template.py \ --template templates/official/prisma2020/PRISMA_2020_flow_new_v2.pptx \ --counts-file my_counts.json \ --out fig1_prisma_filled.pptx """ from __future__ import annotations import argparse import json import re from pathlib import Path from pptx import Presentation POSITIONAL_KEYS = [ "n_db", "n_dup", "n_screened", "n_screen_excluded", "n_sought", "n_assessed", "n_excl_r1", "n_excl_r2", "n_excl_r3", "n_studies", ] ALL_KNOWN_KEYS = { "n_db", "n_reg", "n_dup", "n_auto", "n_other_removed", "n_web", "n_org", "n_cite", "n_screened", "n_screen_excluded", "n_sought", "n_not_retrieved", "n_assessed", "n_excl_r1", "n_excl_r2", "n_excl_r3", "n_studies", "n_reports", } TOKEN_RE = re.compile(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}") def parse_counts(args) -> dict[str, str]: counts: dict[str, str] = {} if args.counts_file: data = json.loads(Path(args.counts_file).read_text()) if not isinstance(data, dict): raise SystemExit("--counts-file must contain a JSON object mapping key→count") counts.update({k: str(v) for k, v in data.items()}) if args.counts: values = [v.strip() for v in args.counts.split(",")] if len(values) != len(POSITIONAL_KEYS): raise SystemExit( f"--counts expects {len(POSITIONAL_KEYS)} comma-separated values " f"(got {len(values)}). Order: {', '.join(POSITIONAL_KEYS)}" ) for k, v in zip(POSITIONAL_KEYS, values): counts.setdefault(k, v) return counts def substitute(prs, counts: dict[str, str]) -> tuple[set[str], set[str]]: """Replace ``{key}`` tokens in every text run. Returns (filled, unfilled).""" filled: set[str] = set() unfilled: set[str] = set() for slide in prs.slides: for shape in slide.shapes: if not shape.has_text_frame: continue for para in shape.text_frame.paragraphs: for run in para.runs: tokens = TOKEN_RE.findall(run.text) if not tokens: continue new_text = run.text for tok in tokens: if tok in counts: new_text = new_text.replace("{" + tok + "}", counts[tok]) filled.add(tok) else: new_text = new_text.replace("{" + tok + "}", "-") unfilled.add(tok) run.text = new_text return filled, unfilled def main(): ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("--template", type=Path, required=True) ap.add_argument("--counts", help="Comma-separated positional counts (10 values).") ap.add_argument("--counts-file", type=Path, help="JSON file mapping key→count.") ap.add_argument("--out", type=Path, required=True) args = ap.parse_args() if not args.counts and not args.counts_file: ap.error("supply --counts or --counts-file (or both)") counts = parse_counts(args) unknown = set(counts) - ALL_KNOWN_KEYS if unknown: print(f"⚠ unknown keys (will still substitute if matched): {sorted(unknown)}") prs = Presentation(args.template) filled, unfilled = substitute(prs, counts) args.out.parent.mkdir(parents=True, exist_ok=True) prs.save(args.out) print(f"✓ wrote {args.out}") print(f" filled : {sorted(filled)}") if unfilled: print(f" blanks : {sorted(unfilled)} (rendered as '-')") if __name__ == "__main__": main() -
generate_flow_diagram.R 5 KB · in bundle
-
generate_image.py 3.4 KB
#!/usr/bin/env python3 """ AI Image Generation via Gemini API (optional supplementary tool). Visual abstracts and figures can be created without this tool. Usage: python generate_image.py "prompt text" --output path/to/output.png python generate_image.py "prompt text" --output path/to/output.png --aspect 16:9 python generate_image.py "CT-guided lung biopsy procedure" -o biopsy.png --style medical Environment variable required: GEMINI_API_KEY """ import argparse import base64 import os import sys from pathlib import Path MEDICAL_STYLE_PREFIX = ( "Create a clean medical illustration in the style of Servier Medical Art. " "Use flat vector-style graphics with clear outlines. No text in the image. " "White or transparent background. Anatomically accurate but simplified. " "Suitable for use in a peer-reviewed journal graphical abstract. " ) def generate_image(prompt: str, output_path: str, aspect_ratio: str = "1:1", style: str | None = None) -> str: import google.generativeai as genai genai.configure(api_key=os.environ["GEMINI_API_KEY"]) model = genai.GenerativeModel("gemini-2.0-flash-preview-image-generation") # Prepend medical style prefix if requested full_prompt = prompt if style == "medical": full_prompt = MEDICAL_STYLE_PREFIX + prompt # Include aspect ratio instruction in the prompt if aspect_ratio != "1:1": full_prompt += f" Output aspect ratio: {aspect_ratio}." response = model.generate_content( full_prompt, generation_config=genai.types.GenerationConfig( response_mime_type="image/png", ), ) # Extract image data from response if response.candidates and response.candidates[0].content.parts: for part in response.candidates[0].content.parts: if hasattr(part, "inline_data") and part.inline_data: image_data = part.inline_data.data Path(output_path).write_bytes(image_data) print(f"Image saved: {output_path}", file=sys.stderr) print(output_path) return output_path # Fallback: check if response has image in different format print("Error: No image data in response.", file=sys.stderr) if response.text: print(f"Response text: {response.text[:200]}", file=sys.stderr) sys.exit(1) def main(): parser = argparse.ArgumentParser(description="Generate images via Gemini API") parser.add_argument("prompt", help="Image generation prompt") parser.add_argument("--output", "-o", required=True, help="Output file path (.png)") parser.add_argument( "--aspect", default="1:1", choices=["1:1", "16:9", "9:16", "4:3", "3:4"], help="Aspect ratio (default: 1:1)", ) parser.add_argument( "--style", choices=["medical"], help="Prepend a style prefix to the prompt (medical: flat vector medical illustration)", ) args = parser.parse_args() if not os.environ.get("GEMINI_API_KEY"): print("Error: GEMINI_API_KEY not set", file=sys.stderr) print("This tool is optional — visual abstracts can be created without it.", file=sys.stderr) sys.exit(1) output_path = Path(args.output).resolve() output_path.parent.mkdir(parents=True, exist_ok=True) generate_image(args.prompt, str(output_path), args.aspect, args.style) if __name__ == "__main__": main() -
generate_visual_abstract.py 16.8 KB
#!/usr/bin/env python3 """ Visual Abstract Generator for Medical Research Papers. Fills journal-specific PPTX templates with study content using python-pptx. Usage: python generate_visual_abstract.py \ --template european_radiology.pptx \ --title "Effect of smoking on biopsy outcomes" \ --hypothesis "What is the association of smoking with PTNB outcomes?" \ --methods "Retrospective cohort|N=1200 patients|Logistic regression" \ --finding "Smoking was associated with higher complication rates" \ --citation "Journal Name (2026) FirstAuthor Last et al; DOI: 10.xxxx/..." \ --visual figures/fig1_roc_curve.png \ --badges "N=1200|CT chest|Single-center" \ --output visual_abstract.pptx # With PNG conversion (requires LibreOffice): python generate_visual_abstract.py ... --output visual_abstract.pptx --png Templates are stored in: ${CLAUDE_SKILL_DIR}/references/visual_abstract_templates/ If --template is a bare name (no path), the script looks for it in the templates directory. If not found, falls back to medsci_default.pptx. """ import argparse import os import re import subprocess import sys from pathlib import Path # --- Shape matching patterns --- # Each field maps to a list of substrings to search in shape.text (case-insensitive). FIELD_PATTERNS = { "title": ["articletitle"], "hypothesis": ["hypothesis", "question"], "methods": ["methodology", "flowchart", "bullet point"], "visual": ["visual element", "image/illustration", "illustration/graph", "visualelement"], "finding": ["main finding", "relevance statement", "main result"], "citation": ["authors names", "doi", "eur radiol (year)", "journal (year)", "articlecitation"], "badge_patient": ["patient cohort", "patient"], "badge_modality": ["modality", "organ"], "badge_center": ["single", "multi-center", "center"], "footer": ["footernote"], } # --- Central Illustration validation rules (Fuster-Mann 2019, JACC 74:2816) --- # See references/jacc_central_illustration_principles.md for full rationale. CI_FORBIDDEN_METHODS_TERMS = ( "cohort flow", "inclusion criteria", "exclusion criteria", "study design", "enrollment", "randomized", "sample size", "consort", "prisma", "stard", ) def validate_central_illustration(zones: int, label_words: int, numerical_points: int, raw_text: str, allow_overrides: list[str] | None = None) -> tuple[bool, list[str]]: """Apply CI mode validation rules. Returns (passes, reasons).""" allow = set(allow_overrides or []) failures: list[str] = [] if "zones" not in allow and zones > 3: failures.append( f"more than 3 visual zones (got {zones}); Fuster-Mann: 'simplicity is superior'" ) if "words" not in allow and label_words > 30: failures.append( f"label word count {label_words} > 30; Fuster-Mann: 'avoid using too much text'" ) if "numerical" not in allow and numerical_points > 4: failures.append( f"{numerical_points} numerical highlights > 4; Fuster-Mann: 'avoid secondary messages'" ) if "methods" not in allow: lower = (raw_text or "").lower() offenders = [t for t in CI_FORBIDDEN_METHODS_TERMS if t in lower] if offenders: failures.append( f"methodology terms detected ({', '.join(offenders)}); CI is not a Visual Abstract" ) return (len(failures) == 0, failures) def match_field(shape_text: str) -> str | None: """Return the field name that matches the shape's text, or None.""" text_lower = shape_text.lower().strip() if not text_lower: return None for field, patterns in FIELD_PATTERNS.items(): for pattern in patterns: if pattern in text_lower: return field return None def replace_shape_text(shape, new_text: str, preserve_formatting: bool = True): """Replace all text in a shape while preserving font formatting.""" tf = shape.text_frame if not tf.paragraphs: return if preserve_formatting and tf.paragraphs[0].runs: # Preserve the first run's formatting first_run = tf.paragraphs[0].runs[0] font_name = first_run.font.name font_size = first_run.font.size font_bold = first_run.font.bold try: font_color = first_run.font.color.rgb except (AttributeError, TypeError): font_color = None # Handle multi-line text (split by \n) lines = new_text.split("\n") # Clear all existing paragraphs except first for i in range(len(tf.paragraphs) - 1, 0, -1): p = tf.paragraphs[i]._p p.getparent().remove(p) # Set first paragraph para = tf.paragraphs[0] para.clear() run = para.add_run() run.text = lines[0] if font_name: run.font.name = font_name if font_size: run.font.size = font_size if font_bold is not None: run.font.bold = font_bold if font_color: run.font.color.rgb = font_color # Add remaining lines as new paragraphs for line in lines[1:]: from pptx.oxml.ns import qn from copy import deepcopy new_p = deepcopy(para._p) new_p.clear() # Re-add run with text from lxml import etree r_elem = deepcopy(para._p.findall(qn("a:r"))[0]) if para._p.findall(qn("a:r")) else None if r_elem is not None: for t in r_elem.findall(qn("a:t")): t.text = line new_p.append(r_elem) tf._txBody.append(new_p) else: # Simple replacement without formatting preservation tf.paragraphs[0].text = new_text def insert_image_into_shape(slide, shape, image_path: str): """Replace a shape with an image, maintaining the shape's position and size.""" from pptx.util import Emu from PIL import Image # Get shape bounds left = shape.left top = shape.top box_w = shape.width box_h = shape.height # Calculate image dimensions maintaining aspect ratio with Image.open(image_path) as img: img_w, img_h = img.size aspect = img_w / img_h box_aspect = box_w / box_h if aspect > box_aspect: # Image is wider than box — fit to width new_w = box_w new_h = int(box_w / aspect) new_left = left new_top = top + (box_h - new_h) // 2 # center vertically else: # Image is taller — fit to height new_h = box_h new_w = int(box_h * aspect) new_left = left + (box_w - new_w) // 2 # center horizontally new_top = top # Clear shape text if hasattr(shape, "text_frame"): for para in shape.text_frame.paragraphs: para.clear() # Add image on top of shape slide.shapes.add_picture(image_path, new_left, new_top, new_w, new_h) def fill_template(template_path: str, content: dict, output_path: str, visual_path: str | None = None, slide_index: int = 0): """Fill a PPTX template with content and save.""" from pptx import Presentation prs = Presentation(template_path) if slide_index >= len(prs.slides): print(f"Error: Slide index {slide_index} not found in template " f"(template has {len(prs.slides)} slides)", file=sys.stderr) sys.exit(1) slide = prs.slides[slide_index] matched_fields = set() for shape in slide.shapes: if not hasattr(shape, "text"): continue field = match_field(shape.text) if field is None: continue if field == "visual" and visual_path and Path(visual_path).exists(): insert_image_into_shape(slide, shape, visual_path) matched_fields.add(field) elif field in content and content[field]: replace_shape_text(shape, content[field]) matched_fields.add(field) # Report matching results all_fields = set(content.keys()) if visual_path: all_fields.add("visual") unmatched = all_fields - matched_fields if unmatched: print(f"Warning: No matching shape found for: {', '.join(unmatched)}", file=sys.stderr) # Remove unused slides (keep only the target slide) # python-pptx slide deletion requires direct XML manipulation sldIdLst = prs.slides._sldIdLst indices_to_remove = sorted( [i for i in range(len(sldIdLst)) if i != slide_index], reverse=True # remove from end to preserve indices ) for i in indices_to_remove: sldId = sldIdLst[i] rId = sldId.rId sldIdLst.remove(sldId) try: prs.part.drop_rel(rId) except KeyError: pass # relationship already cleaned up prs.save(output_path) print(f"Visual abstract saved: {output_path}", file=sys.stderr) print(output_path) def convert_to_png(pptx_path: str, dpi: int = 300) -> str | None: """Convert PPTX to PNG using LibreOffice CLI. Returns PNG path or None.""" output_dir = str(Path(pptx_path).parent) try: subprocess.run( ["soffice", "--headless", "--convert-to", "png", pptx_path, "--outdir", output_dir], check=True, capture_output=True, timeout=30 ) png_path = str(Path(pptx_path).with_suffix(".png")) if Path(png_path).exists(): print(f"PNG exported: {png_path}", file=sys.stderr) return png_path except FileNotFoundError: print("Warning: LibreOffice not found. PPTX saved but PNG conversion skipped.", file=sys.stderr) print("Install with: brew install --cask libreoffice", file=sys.stderr) except subprocess.TimeoutExpired: print("Warning: LibreOffice conversion timed out.", file=sys.stderr) except subprocess.CalledProcessError as e: print(f"Warning: LibreOffice conversion failed: {e.stderr.decode()[:200]}", file=sys.stderr) return None def resolve_template(template_arg: str) -> str: """Resolve template name to full path.""" # If it's already a full path, use it if os.path.isabs(template_arg) and Path(template_arg).exists(): return template_arg # Look in the skill's template directory skill_dir = os.environ.get("CLAUDE_SKILL_DIR", "") if skill_dir: templates_dir = Path(skill_dir) / "references" / "visual_abstract_templates" else: templates_dir = Path(__file__).parent.parent / "references" / "visual_abstract_templates" # Try exact match candidate = templates_dir / template_arg if candidate.exists(): return str(candidate) # Try with .pptx extension candidate = templates_dir / f"{template_arg}.pptx" if candidate.exists(): return str(candidate) # Fallback to medsci_default default = templates_dir / "medsci_default.pptx" if default.exists(): print(f"Template '{template_arg}' not found, using medsci_default.pptx", file=sys.stderr) return str(default) print(f"Error: No template found. Searched in: {templates_dir}", file=sys.stderr) sys.exit(1) def parse_methods(methods_str: str) -> str: """Convert pipe-separated methods to bullet-point format.""" items = [m.strip() for m in methods_str.split("|") if m.strip()] if len(items) <= 1: return methods_str return "\n".join(f"• {item}" for item in items) def main(): parser = argparse.ArgumentParser( description="Generate visual abstract from PPTX template" ) parser.add_argument("--type", choices=["visual-abstract", "central-illustration"], default="visual-abstract", help="Artifact type: 'visual-abstract' (methods+results) or " "'central-illustration' (single key finding, JACC house style)") parser.add_argument("--template", "-t", default=None, help="Template name or path. Default depends on --type: " "visual-abstract→medsci_default, central-illustration→jacc_central_illustration") parser.add_argument("--title", help="Article title (required for VA mode)") parser.add_argument("--hypothesis", help="Research question or hypothesis (VA mode)") parser.add_argument("--methods", help="Methodology (pipe-separated for bullets, VA mode)") parser.add_argument("--finding", help="Main finding (<20 words, VA mode)") parser.add_argument("--citation", help="Citation line (journal, year, authors, DOI)") parser.add_argument("--visual", help="Path to visual element image (PNG/JPG)") parser.add_argument("--badges", help="Three pipe-separated badge texts: cohort|modality|center (VA mode)") parser.add_argument("--output", "-o", required=True, help="Output PPTX path") parser.add_argument("--png", action="store_true", help="Also convert to PNG (requires LibreOffice)") parser.add_argument("--slide-index", type=int, default=0, help="Template slide index to use (default: 0)") parser.add_argument("--ci-zones", type=int, default=None, help="CI mode: declared visual zone count (for validation)") parser.add_argument("--ci-label-words", type=int, default=None, help="CI mode: total label word count (for validation)") parser.add_argument("--ci-numerical-points", type=int, default=None, help="CI mode: count of numerical highlights (for validation)") parser.add_argument("--ci-raw-text", default="", help="CI mode: raw text content for forbidden-methods-term scan") parser.add_argument("--ci-allow", action="append", default=[], choices=["zones", "words", "numerical", "methods"], help="CI mode: override a single rule (repeatable)") args = parser.parse_args() # Default template per type if args.template is None: args.template = ("jacc_central_illustration" if args.type == "central-illustration" else "medsci_default") # CI mode: run validation BEFORE generating the PPTX if args.type == "central-illustration": zones = args.ci_zones if args.ci_zones is not None else 1 words = args.ci_label_words if args.ci_label_words is not None else 0 nums = args.ci_numerical_points if args.ci_numerical_points is not None else 0 passes, reasons = validate_central_illustration( zones=zones, label_words=words, numerical_points=nums, raw_text=args.ci_raw_text, allow_overrides=args.ci_allow, ) if not passes: print("Central Illustration validation FAILED:", file=sys.stderr) for r in reasons: print(f" - {r}", file=sys.stderr) print("\nSee references/jacc_central_illustration_principles.md for guidance.", file=sys.stderr) print("Override with --ci-allow {zones|words|numerical|methods} (use sparingly).", file=sys.stderr) sys.exit(2) # CI mode does not require --title (only --visual + --citation) if not args.visual: print("CI mode requires --visual (the author content figure).", file=sys.stderr) sys.exit(2) # Synthesize a placeholder title so downstream code does not fail if not args.title: args.title = "(central illustration — title carried by manuscript)" # Resolve template template_path = resolve_template(args.template) # Build content dict content = { "title": args.title, "hypothesis": args.hypothesis or "", "methods": parse_methods(args.methods) if args.methods else "", "finding": args.finding or "", "citation": args.citation or "", } # Parse badges if args.badges: badges = [b.strip() for b in args.badges.split("|")] content["badge_patient"] = badges[0] if len(badges) > 0 else "" content["badge_modality"] = badges[1] if len(badges) > 1 else "" content["badge_center"] = badges[2] if len(badges) > 2 else "" # Validate visual path visual_path = None if args.visual: if not Path(args.visual).exists(): print(f"Warning: Visual file not found: {args.visual}", file=sys.stderr) else: visual_path = str(Path(args.visual).resolve()) # Ensure output directory exists Path(args.output).parent.mkdir(parents=True, exist_ok=True) # Fill template fill_template(template_path, content, args.output, visual_path, args.slide_index) # Optional PNG conversion if args.png: convert_to_png(args.output) if __name__ == "__main__": main() -
render_core_figures.py 29 KB
#!/usr/bin/env python3 """Render the four highest-yield clinical figures as tested, deterministic generators. This is the *render* layer for the canonical figure anatomies described in prose under ``references/exemplar_plots/`` (km_curve / roc_pr / calibration_plot / decision_curve). It turns those anatomy models into RUNNABLE code so a regression in a publication figure is caught by a test, not by a reviewer — the gap that left make-figures with no deterministic render check for any data plot. Separation of concerns (do not duplicate the analysis SoT): - ``/analyze-stats`` *computes* the estimates (KM via lifelines, AUC, calibration slope/intercept, net benefit). Those numbers are the single source of truth. - this module *renders* already-computed inputs into the canonical anatomy, and asserts the load-bearing elements are present. It never recomputes a statistic and never invents a number. Each renderer returns a matplotlib ``Figure`` so a verifier can introspect the actual artists (lines, texts, collections) rather than pixel-diffing a PNG. ``assert_structure`` encodes the load-bearing-element invariants for each figure type. Matplotlib (Agg, headless) + numpy only — no seaborn, no network, no RNG at import. CLI: render_core_figures.py --inputs fixture/synthetic_inputs.json --out-dir OUT (renders all four PNGs, runs every structural assertion; exit 1 on any failure). """ from __future__ import annotations import argparse import json import sys from pathlib import Path import matplotlib matplotlib.use("Agg") # headless, deterministic import matplotlib.pyplot as plt # noqa: E402 import numpy as np # noqa: E402 from matplotlib.patches import Polygon # noqa: E402 # --------------------------------------------------------------------------- KM def km_curve(groups: list[dict], max_follow_up: float, *, title: str = "") -> plt.Figure: """Kaplan–Meier step curve(s) with a number-at-risk table and censor marks. groups: [{name, time:[...], surv:[0..1], censor_times:[...], at_risk:{time:[...], n:[...]}}] — already-computed step coordinates. The x-axis is clipped to ``max_follow_up`` so the curve is never extrapolated past observed follow-up (a recurrent KM error the exemplar anatomy warns against). """ fig, (ax, ax_risk) = plt.subplots( 2, 1, figsize=(6.4, 5.2), gridspec_kw={"height_ratios": [4, 1]} ) for g in groups: t = np.asarray(g["time"], float) s = np.asarray(g["surv"], float) line, = ax.step(t, s, where="post", label=g["name"]) # censor tick marks on the step curve for ct in g.get("censor_times", []): idx = np.searchsorted(t, ct, side="right") - 1 idx = max(0, min(idx, len(s) - 1)) ax.plot([ct], [s[idx]], marker="|", markersize=8, color=line.get_color()) ax.set_xlim(0, max_follow_up) # no extrapolation past follow-up ax.set_ylim(0, 1.02) ax.set_ylabel("Survival probability") ax.set_title(title or "Kaplan–Meier survival") ax.legend(loc="lower left", frameon=False) # number-at-risk table on the lower axes ax_risk.axis("off") ax_risk.set_xlim(0, max_follow_up) ax_risk.text(0, len(groups) + 0.5, "No. at risk", fontsize=9, fontweight="bold", transform=ax_risk.get_yaxis_transform()) for row, g in enumerate(groups): risk = g["at_risk"] y = len(groups) - row - 0.5 for tt, nn in zip(risk["time"], risk["n"]): ax_risk.text(tt, y, str(int(nn)), ha="center", va="center", fontsize=8) ax_risk.text(0, y, g["name"], ha="right", va="center", fontsize=8, transform=ax_risk.get_yaxis_transform()) ax_risk.set_xlabel("Time (months)") fig.tight_layout() fig._mf_kind = "km" # tag for assert_structure fig._mf_max_follow_up = max_follow_up return fig # -------------------------------------------------------------------------- ROC def roc_curve(fpr, tpr, auc: float, *, operating_point: dict | None = None, title: str = "") -> plt.Figure: """ROC curve with the chance diagonal, an AUC annotation, and an operating point.""" fpr = np.asarray(fpr, float) tpr = np.asarray(tpr, float) fig, ax = plt.subplots(figsize=(5.2, 5.0)) ax.plot(fpr, tpr, label=f"Model (AUC = {auc:.3f})") ax.plot([0, 1], [0, 1], linestyle="--", color="0.5", label="Chance") # diagonal if operating_point: ax.scatter([operating_point["fpr"]], [operating_point["tpr"]], color="crimson", zorder=5, s=40, label=operating_point.get("label", "Operating point")) ax.set_xlim(0, 1) ax.set_ylim(0, 1.02) ax.set_xlabel("1 − specificity (false-positive rate)") ax.set_ylabel("Sensitivity (true-positive rate)") ax.set_title(title or "Receiver operating characteristic") ax.text(0.55, 0.10, f"AUC = {auc:.3f}", fontsize=11, bbox=dict(boxstyle="round", fc="white", ec="0.7")) ax.legend(loc="lower right", frameon=False) fig.tight_layout() fig._mf_kind = "roc" return fig # ------------------------------------------------------------------ calibration def calibration_plot(pred_mean, obs_freq, *, slope: float, intercept: float, ci_low=None, ci_high=None, title: str = "") -> plt.Figure: """Calibration plot: binned observed-vs-predicted, the y=x identity line, and the fitted calibration slope/intercept annotation (the load-bearing calibration metrics).""" pred_mean = np.asarray(pred_mean, float) obs_freq = np.asarray(obs_freq, float) fig, ax = plt.subplots(figsize=(5.2, 5.2)) ax.plot([0, 1], [0, 1], linestyle="--", color="0.5", label="Ideal (y = x)") # identity if ci_low is not None and ci_high is not None: yerr = np.vstack([obs_freq - np.asarray(ci_low, float), np.asarray(ci_high, float) - obs_freq]) ax.errorbar(pred_mean, obs_freq, yerr=yerr, fmt="o", capsize=3, label="Observed") else: ax.plot(pred_mean, obs_freq, marker="o", label="Observed") # fitted calibration line xs = np.linspace(0, 1, 50) ax.plot(xs, intercept + slope * xs, color="crimson", label="Calibration fit") ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.set_xlabel("Predicted probability") ax.set_ylabel("Observed frequency") ax.set_title(title or "Calibration") ax.text(0.05, 0.88, f"slope = {slope:.2f}\nintercept = {intercept:.2f}", fontsize=10, bbox=dict(boxstyle="round", fc="white", ec="0.7")) ax.legend(loc="lower right", frameon=False) fig.tight_layout() fig._mf_kind = "calibration" return fig # --------------------------------------------------------------- decision curve def decision_curve(thresholds, net_benefit_model, prevalence: float, *, model_label: str = "Model", title: str = "") -> plt.Figure: """Decision-curve (net-benefit) plot with the treat-all and treat-none reference strategies — the two references without which a DCA is uninterpretable.""" pt = np.asarray(thresholds, float) nb_model = np.asarray(net_benefit_model, float) # treat-all net benefit = prev - (1-prev) * pt/(1-pt); treat-none = 0 with np.errstate(divide="ignore", invalid="ignore"): nb_all = prevalence - (1 - prevalence) * pt / (1 - pt) fig, ax = plt.subplots(figsize=(6.0, 4.8)) ax.plot(pt, nb_model, label=model_label, color="crimson") ax.plot(pt, nb_all, label="Treat all", color="0.4", linestyle="-.") ax.plot(pt, np.zeros_like(pt), label="Treat none", color="0.0", linestyle=":") ax.set_xlim(pt.min(), pt.max()) lo = float(min(0.0, np.nanmin(nb_model))) ax.set_ylim(lo - 0.02, float(np.nanmax(nb_model)) + 0.05) ax.set_xlabel("Threshold probability") ax.set_ylabel("Net benefit") ax.set_title(title or "Decision-curve analysis") ax.legend(loc="upper right", frameon=False) fig.tight_layout() fig._mf_kind = "dca" return fig # ------------------------------------------------------------------- forest def forest_plot(studies: list[dict], pooled: dict, *, null_value: float = 1.0, effect_label: str = "Effect (95% CI)", title: str = "", log_x: bool = True) -> plt.Figure: """Meta-analysis forest plot from already-computed per-study estimates. studies: [{name, est, lo, hi, weight?}] — each study's point estimate + CI. pooled: {est, lo, hi, label} — the pooled estimate + CI + model name. Draws a weight-scaled marker + CI whisker per study, the null reference line, and a pooled diamond; the pooled row is always last. For a ratio measure keep log_x=True and null_value=1.0; for a mean difference pass log_x=False, null_value=0.0.""" n = len(studies) fig, ax = plt.subplots(figsize=(6.6, 0.5 * n + 2.0)) ys = list(range(n, 0, -1)) # top study at the highest y weights = np.asarray([s.get("weight", 1.0) for s in studies], float) wnorm = weights / weights.max() if weights.max() > 0 else np.ones(n) for y, s, w in zip(ys, studies, wnorm): ax.plot([s["lo"], s["hi"]], [y, y], color="0.3") # CI whisker ax.scatter([s["est"]], [y], s=30 + 120 * w, marker="s", color="steelblue", zorder=4) # weight-scaled box ax.axvline(null_value, linestyle="--", color="0.5") # null reference # pooled diamond on a row below the studies yd = 0 d = pooled ax.add_patch(Polygon([[d["lo"], yd], [d["est"], yd + 0.35], [d["hi"], yd], [d["est"], yd - 0.35]], closed=True, facecolor="crimson", edgecolor="black", zorder=5)) labels = [s["name"] for s in studies] + [pooled.get("label", "Pooled")] ax.set_yticks(ys + [yd]) ax.set_yticklabels(labels) ax.set_ylim(-1, n + 1) if log_x: ax.set_xscale("log") ax.set_xlabel(effect_label) ax.set_title(title or "Meta-analysis forest plot") fig.tight_layout() fig._mf_kind = "forest" fig._mf_n_studies = n fig._mf_null = null_value return fig # -------------------------------------------------------------- Bland–Altman def bland_altman(mean_vals, diff_vals, *, bias: float, sd_diff: float, title: str = "") -> plt.Figure: """Bland–Altman agreement plot: difference vs mean, with the bias line and the 95% limits of agreement (bias ± 1.96·SD) — the load-bearing agreement elements.""" mean_vals = np.asarray(mean_vals, float) diff_vals = np.asarray(diff_vals, float) loa_hi, loa_lo = bias + 1.96 * sd_diff, bias - 1.96 * sd_diff fig, ax = plt.subplots(figsize=(6.0, 5.0)) ax.scatter(mean_vals, diff_vals, s=25, color="steelblue", alpha=0.8) ax.axhline(bias, color="crimson", label=f"Bias {bias:.2f}") ax.axhline(loa_hi, linestyle="--", color="0.4", label=f"+1.96 SD {loa_hi:.2f}") ax.axhline(loa_lo, linestyle="--", color="0.4", label=f"−1.96 SD {loa_lo:.2f}") ax.set_xlabel("Mean of the two measurements") ax.set_ylabel("Difference between measurements") ax.set_title(title or "Bland–Altman agreement") ax.legend(loc="upper right", frameon=False) fig.tight_layout() fig._mf_kind = "bland_altman" fig._mf_loa = (loa_lo, loa_hi) return fig # ---------------------------------------------------------- confusion matrix def confusion_matrix(matrix, labels, *, title: str = "") -> plt.Figure: """Confusion matrix from an already-computed count grid. Rows = actual, cols = predicted; every cell is annotated with its count (for a 2×2, TN/FP/FN/TP).""" m = np.asarray(matrix, float) k = m.shape[0] if m.shape[0] != m.shape[1] or k != len(labels): raise AssertionError("confusion matrix must be square and match the label count") fig, ax = plt.subplots(figsize=(1.4 * k + 2, 1.4 * k + 2)) ax.imshow(m, cmap="Blues") thresh = m.max() / 2.0 if m.max() else 0.5 for i in range(k): for j in range(k): ax.text(j, i, str(int(m[i, j])), ha="center", va="center", color="white" if m[i, j] > thresh else "black") ax.set_xticks(range(k)); ax.set_xticklabels(labels) ax.set_yticks(range(k)); ax.set_yticklabels(labels) ax.set_xlabel("Predicted") ax.set_ylabel("Actual") ax.set_title(title or "Confusion matrix") fig.tight_layout() fig._mf_kind = "confusion" fig._mf_k = k return fig # ------------------------------------------------------------------ MRMC ROC def mrmc_roc(readers: list[dict], averaged: dict, *, delta_auc: dict | None = None, title: str = "") -> plt.Figure: """Multi-reader multi-case ROC: each reader's ROC curve plus the reader-averaged curve and the chance diagonal (the load-bearing MRMC-reader-study elements). readers: [{name, fpr, tpr, auc}] — per-reader ROC coordinates. averaged: {fpr, tpr, auc, label} — the reader-averaged curve. delta_auc (optional): {value, margin} — a ΔAUC-vs-margin annotation.""" fig, ax = plt.subplots(figsize=(5.4, 5.2)) for r in readers: ax.plot(np.asarray(r["fpr"], float), np.asarray(r["tpr"], float), color="0.7", linewidth=1) # thin per-reader a = averaged ax.plot(np.asarray(a["fpr"], float), np.asarray(a["tpr"], float), color="crimson", linewidth=2.4, label=f"{a.get('label', 'Reader-averaged')} (AUC = {a['auc']:.3f})") ax.plot([0, 1], [0, 1], linestyle="--", color="0.5", label="Chance") ax.set_xlim(0, 1); ax.set_ylim(0, 1.02) ax.set_xlabel("1 − specificity (false-positive rate)") ax.set_ylabel("Sensitivity (true-positive rate)") ax.set_title(title or "Multi-reader multi-case ROC") txt = f"averaged AUC = {a['auc']:.3f}" if delta_auc: txt += f"\nΔAUC = {delta_auc['value']:.3f} (margin {delta_auc['margin']:.3f})" ax.text(0.55, 0.08, txt, fontsize=10, bbox=dict(boxstyle="round", fc="white", ec="0.7")) ax.legend(loc="lower right", frameon=False) fig.tight_layout() fig._mf_kind = "mrmc_roc" fig._mf_n_readers = len(readers) return fig # ------------------------------------------------------------------ Manhattan def manhattan(x, neglogp, threshold: float, *, labels=None, ylabel: str = "−log10(p)", xlabel: str = "Exposure / position", title: str = "") -> plt.Figure: """Manhattan / *-wide-scan plot: −log10(p) vs position with the significance threshold line (the two load-bearing elements of an agnostic many-test scan).""" x = np.asarray(x, float) y = np.asarray(neglogp, float) fig, ax = plt.subplots(figsize=(7.0, 4.4)) ax.scatter(x, y, s=14, color="steelblue", alpha=0.8) ax.axhline(threshold, color="crimson", linestyle="--", label=f"significance threshold (−log10 = {threshold:.2f})") if labels: # sparse labelling of hits above the threshold for xi, yi, lab in zip(x, y, labels): if lab and yi >= threshold: ax.annotate(lab, (xi, yi), fontsize=8, xytext=(0, 4), textcoords="offset points", ha="center") ax.set_xlabel(xlabel) ax.set_ylabel(ylabel) ax.set_title(title or "Manhattan plot") ax.legend(loc="upper right", frameon=False) fig.tight_layout() fig._mf_kind = "manhattan" fig._mf_threshold = threshold return fig # --------------------------------------------------------- clinical timeline def clinical_timeline(events: list[dict], *, time_unit: str = "days from admission", title: str = "") -> plt.Figure: """Case-report clinical timeline: an event marker + label at each time on a single time axis (the load-bearing elements of a longitudinal case figure).""" times = [float(e["time"]) for e in events] fig, ax = plt.subplots(figsize=(max(6.0, 0.9 * len(events)), 3.4)) lo, hi = (min(times), max(times)) if times else (0, 1) pad = max(1.0, (hi - lo) * 0.08) ax.axhline(0, color="0.4") # the timeline for i, e in enumerate(events): t = float(e["time"]) up = 1 if i % 2 == 0 else -1 ax.plot([t, t], [0, up * 0.6], color="0.6") # stem ax.scatter([t], [0], s=40, color="crimson", zorder=5) # event marker ax.annotate(str(e["label"]), (t, up * 0.65), ha="center", va="bottom" if up > 0 else "top", fontsize=8) ax.set_xlim(lo - pad, hi + pad) ax.set_ylim(-1.3, 1.3) ax.get_yaxis().set_visible(False) for spine in ("left", "right", "top"): ax.spines[spine].set_visible(False) ax.set_xlabel(f"Time ({time_unit})") ax.set_title(title or "Clinical timeline") fig.tight_layout() fig._mf_kind = "timeline" fig._mf_n_events = len(events) return fig # ----------------------------------------------------- structural invariants def assert_structure(fig: plt.Figure) -> list[str]: """Assert the load-bearing elements for the figure's kind. Returns the list of checks that PASSED; raises AssertionError on the first violated invariant.""" kind = getattr(fig, "_mf_kind", None) passed: list[str] = [] def has_text(ax, needle: str) -> bool: n = needle.lower() return any(n in t.get_text().lower() for t in ax.texts) \ or (ax.get_legend() and any(n in t.get_text().lower() for t in ax.get_legend().get_texts())) if kind == "km": ax, ax_risk = fig.axes[0], fig.axes[1] steps = [ln for ln in ax.lines if len(ln.get_xdata()) > 2] assert steps, "KM: no step curve drawn" passed.append("KM step curve present") # no extrapolation past follow-up muf = getattr(fig, "_mf_max_follow_up") assert abs(ax.get_xlim()[1] - muf) < 1e-9, "KM: x-axis extends past follow-up" passed.append("KM x-axis clipped to follow-up (no extrapolation)") # monotonic non-increasing survival on each step curve for ln in steps: y = np.asarray(ln.get_ydata(), float) assert np.all(np.diff(y) <= 1e-9), "KM: survival curve is not non-increasing" passed.append("KM survival monotonic non-increasing") assert any(t.get_text().lower().startswith("no. at risk") or "at risk" in t.get_text().lower() for t in ax_risk.texts), \ "KM: number-at-risk table missing" passed.append("KM number-at-risk table present") assert "survival" in ax.get_ylabel().lower(), "KM: y-label not survival" passed.append("KM survival y-label") elif kind == "roc": ax = fig.axes[0] diag = [ln for ln in ax.lines if len(ln.get_xdata()) == 2 and np.allclose(ln.get_xdata(), [0, 1]) and np.allclose(ln.get_ydata(), [0, 1])] assert diag, "ROC: chance diagonal (0,0)-(1,1) missing" passed.append("ROC chance diagonal present") assert has_text(ax, "auc"), "ROC: AUC annotation missing" passed.append("ROC AUC annotation present") assert ax.collections, "ROC: operating-point marker missing" passed.append("ROC operating point present") assert "sensitiv" in ax.get_ylabel().lower(), "ROC: y-label not sensitivity" passed.append("ROC sensitivity y-label") elif kind == "calibration": ax = fig.axes[0] identity = [ln for ln in ax.lines if len(ln.get_xdata()) == 2 and np.allclose(ln.get_xdata(), [0, 1]) and np.allclose(ln.get_ydata(), [0, 1])] assert identity, "Calibration: identity y=x line missing" passed.append("Calibration identity line present") assert has_text(ax, "slope") and has_text(ax, "intercept"), \ "Calibration: slope/intercept annotation missing" passed.append("Calibration slope+intercept annotation present") assert "predicted" in ax.get_xlabel().lower(), "Calibration: x not predicted" assert "observed" in ax.get_ylabel().lower(), "Calibration: y not observed" passed.append("Calibration predicted-vs-observed axes") elif kind == "dca": ax = fig.axes[0] assert len(ax.lines) >= 3, "DCA: fewer than 3 strategies (model/all/none) drawn" passed.append("DCA has model + treat-all + treat-none") treat_none = [ln for ln in ax.lines if np.allclose(ln.get_ydata(), 0.0)] assert treat_none, "DCA: treat-none (net benefit = 0) reference missing" passed.append("DCA treat-none reference present") assert "net benefit" in ax.get_ylabel().lower(), "DCA: y-label not net benefit" passed.append("DCA net-benefit y-label") assert has_text(ax, "treat all") and has_text(ax, "treat none"), \ "DCA: treat-all / treat-none not labelled" passed.append("DCA reference strategies labelled") elif kind == "forest": ax = fig.axes[0] n = getattr(fig, "_mf_n_studies") # one horizontal CI whisker (2-point line, equal y) per study whiskers = [ln for ln in ax.lines if len(ln.get_xdata()) == 2 and np.allclose(np.diff(ln.get_ydata()), 0.0)] assert len(whiskers) >= n, "forest: missing per-study CI whiskers" passed.append(f"forest per-study CI rows present ({n})") null = getattr(fig, "_mf_null") assert any(len(ln.get_xdata()) == 2 and np.allclose(ln.get_xdata(), [null, null]) for ln in ax.lines), "forest: null reference line missing" passed.append("forest null reference line present") assert any(isinstance(p, Polygon) and len(p.get_xy()) >= 4 for p in ax.patches), \ "forest: pooled diamond missing" passed.append("forest pooled diamond present") assert len(ax.get_yticklabels()) >= n + 1, "forest: study/pooled row labels missing" passed.append("forest study + pooled row labels present") elif kind == "bland_altman": ax = fig.axes[0] assert ax.collections, "Bland–Altman: scatter of differences missing" passed.append("Bland–Altman difference scatter present") hlines = [ln for ln in ax.lines if np.allclose(np.diff(ln.get_ydata()), 0.0)] assert len(hlines) >= 3, "Bland–Altman: need bias + two limits-of-agreement lines" passed.append("Bland–Altman bias + 2 LoA lines present") loa_lo, loa_hi = getattr(fig, "_mf_loa") yvals = [float(ln.get_ydata()[0]) for ln in hlines] assert any(abs(y - loa_hi) < 1e-6 for y in yvals) and any(abs(y - loa_lo) < 1e-6 for y in yvals), \ "Bland–Altman: LoA lines not at bias ± 1.96·SD" passed.append("Bland–Altman LoA at bias ± 1.96·SD") assert "difference" in ax.get_ylabel().lower() and "mean" in ax.get_xlabel().lower(), \ "Bland–Altman: axes not difference-vs-mean" passed.append("Bland–Altman difference-vs-mean axes") elif kind == "confusion": ax = fig.axes[0] k = getattr(fig, "_mf_k") assert ax.images, "confusion: matrix image missing" passed.append("confusion matrix image present") cells = [t for t in ax.texts if t.get_text().strip().lstrip("-").isdigit()] assert len(cells) >= k * k, f"confusion: expected {k * k} annotated cells" passed.append(f"confusion all {k}×{k} cells annotated") assert "predicted" in ax.get_xlabel().lower() and "actual" in ax.get_ylabel().lower(), \ "confusion: axes not Predicted/Actual" passed.append("confusion Predicted/Actual axes") elif kind == "mrmc_roc": ax = fig.axes[0] n = getattr(fig, "_mf_n_readers") curves = [ln for ln in ax.lines if len(ln.get_xdata()) > 2] # multi-point ROC curves assert len(curves) >= n + 1, "MRMC-ROC: fewer curves than readers + averaged" passed.append(f"MRMC-ROC per-reader + averaged curves present ({n}+1)") diag = [ln for ln in ax.lines if len(ln.get_xdata()) == 2 and np.allclose(ln.get_xdata(), [0, 1]) and np.allclose(ln.get_ydata(), [0, 1])] assert diag, "MRMC-ROC: chance diagonal missing" passed.append("MRMC-ROC chance diagonal present") assert has_text(ax, "auc"), "MRMC-ROC: averaged-AUC annotation missing" passed.append("MRMC-ROC averaged-AUC annotation present") assert "sensitiv" in ax.get_ylabel().lower(), "MRMC-ROC: y-label not sensitivity" passed.append("MRMC-ROC sensitivity y-label") elif kind == "manhattan": ax = fig.axes[0] assert ax.collections, "Manhattan: point scatter missing" passed.append("Manhattan scatter present") thr = getattr(fig, "_mf_threshold") assert any(np.allclose(ln.get_ydata(), thr) for ln in ax.lines if np.allclose(np.diff(ln.get_ydata()), 0.0)), \ "Manhattan: significance threshold line missing" passed.append("Manhattan significance threshold line present") yl = ax.get_ylabel().lower() assert "log" in yl and ("10" in yl or "log10" in yl or "−log" in yl or "-log" in yl), \ "Manhattan: y-label not −log10(p)" passed.append("Manhattan −log10(p) y-label") elif kind == "timeline": ax = fig.axes[0] ne = getattr(fig, "_mf_n_events") assert any(np.allclose(ln.get_ydata(), 0.0) for ln in ax.lines), \ "timeline: baseline axis missing" passed.append("timeline baseline present") assert ax.collections, "timeline: event markers missing" passed.append("timeline event markers present") assert len([t for t in ax.texts if t.get_text().strip()]) >= ne, \ "timeline: an event label is missing" passed.append(f"timeline all {ne} event labels present") assert "time" in ax.get_xlabel().lower(), "timeline: x-axis not a time axis" passed.append("timeline time x-axis") else: raise AssertionError(f"unknown figure kind: {kind!r}") return passed # ----------------------------------------------------------------- driver def render_all(inputs: dict, out_dir: Path) -> dict: """Render each figure kind present in ``inputs``, save PNGs, and assert structure. Returns {kind: [passed checks]}. Raises on any structural violation.""" out_dir.mkdir(parents=True, exist_ok=True) results: dict[str, list[str]] = {} builders = { "km": lambda d: km_curve(d["groups"], d["max_follow_up"], title=d.get("title", "")), "roc": lambda d: roc_curve(d["fpr"], d["tpr"], d["auc"], operating_point=d.get("operating_point")), "calibration": lambda d: calibration_plot( d["pred_mean"], d["obs_freq"], slope=d["slope"], intercept=d["intercept"], ci_low=d.get("ci_low"), ci_high=d.get("ci_high")), "dca": lambda d: decision_curve(d["thresholds"], d["net_benefit_model"], d["prevalence"]), "forest": lambda d: forest_plot(d["studies"], d["pooled"], null_value=d.get("null_value", 1.0), effect_label=d.get("effect_label", "Effect (95% CI)"), log_x=d.get("log_x", True)), "bland_altman": lambda d: bland_altman(d["mean_vals"], d["diff_vals"], bias=d["bias"], sd_diff=d["sd_diff"]), "confusion": lambda d: confusion_matrix(d["matrix"], d["labels"]), "mrmc_roc": lambda d: mrmc_roc(d["readers"], d["averaged"], delta_auc=d.get("delta_auc")), "manhattan": lambda d: manhattan(d["x"], d["neglogp"], d["threshold"], labels=d.get("labels"), xlabel=d.get("xlabel", "Exposure / position")), "timeline": lambda d: clinical_timeline(d["events"], time_unit=d.get("time_unit", "days from admission")), } for kind, build in builders.items(): if kind not in inputs: continue fig = build(inputs[kind]) results[kind] = assert_structure(fig) fig.savefig(out_dir / f"{kind}.png", dpi=120) png = out_dir / f"{kind}.png" assert png.exists() and png.stat().st_size > 2000, f"{kind}: PNG not written" plt.close(fig) return results def main(argv=None) -> int: ap = argparse.ArgumentParser(description="Render + structurally verify core clinical figures.") ap.add_argument("--inputs", required=True, help="JSON with km/roc/calibration/dca inputs") ap.add_argument("--out-dir", required=True, help="directory for rendered PNGs") args = ap.parse_args(argv) inputs = json.loads(Path(args.inputs).read_text(encoding="utf-8")) try: results = render_all(inputs, Path(args.out_dir)) except AssertionError as e: print(f"RENDER-FAIL: {e}", file=sys.stderr) return 1 for kind, checks in results.items(): print(f"OK [{kind}] {len(checks)} structural invariants: {'; '.join(checks)}") print(f"PASS: {len(results)} figure(s) rendered + structurally verified.") return 0 if __name__ == "__main__": raise SystemExit(main()) -
validate_pptx_mac_compat.py 8.5 KB
#!/usr/bin/env python3 """Validate a .pptx for Mac PowerPoint compatibility. Detects four classes of defect that PowerPoint Mac renders differently from PowerPoint Windows / Keynote / LibreOffice and that PDF export alone does not catch: 1. TIFF images embedded in ppt/media/ — Mac PowerPoint silently drops them. 2. <a:sp3d> 3-D bevel inside text rPr — Mac renders as red outline that does not appear in PDF or on Windows. 3. docProps/app.xml count mismatch with actual slide count — triggers a "PowerPoint found a problem" recovery dialog on first open. 4. <a:srcRect> values exceeding 100000 (1/1000-percent) — image is over-cropped (sometimes 99 % cut off) only on Mac. Reference rule: ~/.claude/rules/pptx-mac-compatibility.md. Usage: python validate_pptx_mac_compat.py path/to/deck.pptx python validate_pptx_mac_compat.py deck.pptx --json out.json python validate_pptx_mac_compat.py deck.pptx --strict # exit 1 on any FAIL Exit codes: 0 — all checks PASS 1 — at least one FAIL (only with --strict; otherwise still 0 with WARN) 2 — input invalid (file missing, not a zip, etc.) """ from __future__ import annotations import argparse import json import re import sys import zipfile from pathlib import Path SP3D_RE = re.compile(r"<a:sp3d\b", re.IGNORECASE) SRC_RECT_RE = re.compile( r'<a:srcRect\b[^/>]*?(?:l|t|r|b)="(\d+)"', re.IGNORECASE ) APP_XML_SLIDES_RE = re.compile(r"<Slides>(\d+)</Slides>", re.IGNORECASE) APP_XML_VECTOR_SIZE_RE = re.compile( r'<vt:vector\b[^>]*?size="(\d+)"', re.IGNORECASE ) def _check_tiff(zf: zipfile.ZipFile) -> list[str]: found = [ name for name in zf.namelist() if name.lower().startswith("ppt/media/") and name.lower().endswith((".tif", ".tiff")) ] return found def _check_sp3d(zf: zipfile.ZipFile) -> list[tuple[str, int]]: hits: list[tuple[str, int]] = [] for name in zf.namelist(): if not (name.startswith("ppt/slides/") and name.endswith(".xml")): continue try: text = zf.read(name).decode("utf-8", errors="replace") except Exception: continue count = len(SP3D_RE.findall(text)) if count > 0: hits.append((name, count)) return hits def _check_app_xml(zf: zipfile.ZipFile) -> dict: result = {"present": False, "declared_slides": None, "actual_slides": None, "vector_size_max": None, "mismatch": False} actual = sum( 1 for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml") ) result["actual_slides"] = actual try: app = zf.read("docProps/app.xml").decode("utf-8", errors="replace") except KeyError: return result result["present"] = True m = APP_XML_SLIDES_RE.search(app) if m: declared = int(m.group(1)) result["declared_slides"] = declared if declared != actual: result["mismatch"] = True sizes = [int(s) for s in APP_XML_VECTOR_SIZE_RE.findall(app)] if sizes: result["vector_size_max"] = max(sizes) return result def _check_src_rect(zf: zipfile.ZipFile) -> list[tuple[str, int]]: """Return [(slide_path, max_value)] for any srcRect value > 100000. srcRect uses 1/1000-percent units; legitimate values are 0–100000. Anything above 100000 indicates a unit-conversion bug (e.g., a percent-to-1/1000-percent miscalculation) and produces severe over-crop on Mac PowerPoint. """ hits: list[tuple[str, int]] = [] for name in zf.namelist(): if not (name.startswith("ppt/slides/") and name.endswith(".xml")): continue try: text = zf.read(name).decode("utf-8", errors="replace") except Exception: continue values = [int(v) for v in SRC_RECT_RE.findall(text)] bad = [v for v in values if v > 100000] if bad: hits.append((name, max(bad))) return hits def validate(pptx_path: Path) -> dict: if not pptx_path.exists(): return {"ok": False, "error": f"file not found: {pptx_path}"} if not zipfile.is_zipfile(pptx_path): return {"ok": False, "error": f"not a zip / pptx: {pptx_path}"} with zipfile.ZipFile(pptx_path) as zf: tiffs = _check_tiff(zf) sp3d = _check_sp3d(zf) app = _check_app_xml(zf) srcrect = _check_src_rect(zf) findings: list[dict] = [] if tiffs: findings.append({ "id": "TIFF", "severity": "FAIL", "message": f"{len(tiffs)} TIFF image(s) embedded; Mac PowerPoint will silently drop these.", "files": tiffs, "fix": "Convert each to PNG (`sips -s format png in.tif --out out.png` on macOS) and update _rels/*.rels to reference the new .png filenames.", }) if sp3d: total = sum(c for _, c in sp3d) findings.append({ "id": "SP3D", "severity": "FAIL", "message": f"<a:sp3d> 3-D bevel found in {len(sp3d)} slide(s) ({total} occurrence(s)); renders as red outline on Mac PowerPoint and is invisible in PDF export.", "files": [{"slide": n, "count": c} for n, c in sp3d], "fix": "Strip <a:sp3d>...</a:sp3d> blocks via regex in each slide XML. Verify on Mac PowerPoint after fix.", }) if app["present"] and app["mismatch"]: findings.append({ "id": "APP_XML_COUNT", "severity": "FAIL", "message": f"docProps/app.xml declares <Slides>{app['declared_slides']}</Slides> but the package contains {app['actual_slides']} slide XML files; PowerPoint Mac will show a 'recover file' dialog on first open.", "fix": "Update <Slides>, <HeadingPairs>, and <TitlesOfParts> in docProps/app.xml to match the actual slide count.", }) elif not app["present"]: findings.append({ "id": "APP_XML_MISSING", "severity": "WARN", "message": "docProps/app.xml not found in package; PowerPoint Mac may show a recovery dialog or open with broken metadata.", "fix": "Re-export the deck from PowerPoint (which writes app.xml) or generate one programmatically.", }) if srcrect: findings.append({ "id": "SRC_RECT_OVERFLOW", "severity": "FAIL", "message": f"<a:srcRect> values >100000 (1/1000-percent maximum) found in {len(srcrect)} slide(s); image will be over-cropped on Mac PowerPoint (often >99 % cut off).", "files": [{"slide": n, "max_value": v} for n, v in srcrect], "fix": "Recompute srcRect: percentages stay 0–100; 1/1000-percent units stay 0–100000. Compare to original .pptx srcRect values if a build script regressed.", }) fail_count = sum(1 for f in findings if f["severity"] == "FAIL") warn_count = sum(1 for f in findings if f["severity"] == "WARN") return { "ok": fail_count == 0, "file": str(pptx_path), "summary": { "fail": fail_count, "warn": warn_count, "slides": app["actual_slides"], }, "findings": findings, } def _format_human(report: dict) -> str: if not report.get("ok") and "error" in report: return f"ERROR: {report['error']}" lines = [f"PPTX Mac compatibility check — {report['file']}"] s = report["summary"] lines.append(f" slides={s['slides']} fail={s['fail']} warn={s['warn']}") if not report["findings"]: lines.append(" ✓ All four checks PASS (TIFF, sp3d, app.xml count, srcRect).") return "\n".join(lines) for f in report["findings"]: lines.append(f" [{f['severity']}] {f['id']}: {f['message']}") lines.append(f" fix: {f['fix']}") return "\n".join(lines) def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) ap.add_argument("pptx", help="path to .pptx") ap.add_argument("--json", help="write JSON report to this path") ap.add_argument("--strict", action="store_true", help="exit 1 if any FAIL (default exits 0 with WARN)") args = ap.parse_args(argv) report = validate(Path(args.pptx)) if not report.get("ok") and "error" in report: print(_format_human(report), file=sys.stderr) return 2 if args.json: Path(args.json).write_text(json.dumps(report, indent=2)) print(_format_human(report)) if args.strict and not report["ok"]: return 1 return 0 if __name__ == "__main__": raise SystemExit(main()) -
_strobe_cascade.py 4.8 KB
#!/usr/bin/env python3 """STROBE participant-flow cascade closure check for a build_strobe_template.py config. A STROBE flow diagram's exclusion cascade must balance: the count in a spine box, minus the exclusions declared after it, must equal the count in the next spine box. A real cohort figure once read "500 excluded -> N = 9,470" while the enrolled box said 10,000, so 10,000 - 500 = 9,500, not 9,470 — a second exclusion, present in the legend, had been dropped from the figure. It survived a full round of peer review and was found only by rendering the submission PDF to an image and reading it by eye, because figure-image numbers are text-grep blind. `check_cohort_arithmetic.py` already asserts this closure in manuscript prose, GFM tables and committed CSVs. The number that a reviewer actually sees, though, lives as text in the flow diagram, generated here from a structured YAML — so the diagram can drift from the prose. This makes the figure carry its own assertion. Low false-positive by construction: a spine link is checked ONLY when at least one exclusion is declared after that box (the author is asserting "A minus these gives B"), and only when every count involved is extractable. A branching Analysis leaf (two boxes sharing a parent, no exclusion between them) is never treated as a cascade step. A box with no "n = …" count is skipped, not guessed. Reused by build_strobe_template.py (a loud warning during the build; fatal under --strict-cascade) and runnable standalone (`_strobe_cascade.py --config figure1.yaml --strict`) so the check travels without python-pptx. """ from __future__ import annotations import argparse import re import sys from pathlib import Path # The box TOTAL is the first "n = X" / "N = X" in the box text — the parenthetical after the # label ("Enrolled (n = 10,000)", "Excluded (n = 500):"). Sub-bullet counts come after it. _COUNT_RE = re.compile(r"[nN]\s*=\s*([\d,]+)") def extract_count(text: str | None) -> int | None: """First `n = X` in the box text as an int, or None when the box carries no count.""" if not text: return None m = _COUNT_RE.search(str(text)) return int(m.group(1).replace(",", "")) if m else None def check_cascade(cfg: dict) -> list[dict]: """Return an imbalance finding for every declared exclusion link A -> B where ``A.count - sum(exclusions after A) != B.count``.""" spine = cfg.get("spine") or [] exclusions = cfg.get("exclusions") or [] if len(spine) < 2: return [] counts = {b.get("id"): extract_count(b.get("text")) for b in spine if isinstance(b, dict)} excl_after: dict[str, list[int | None]] = {} for e in exclusions: if isinstance(e, dict) and e.get("after"): excl_after.setdefault(e["after"], []).append(extract_count(e.get("text"))) findings: list[dict] = [] for i in range(len(spine) - 1): a, b = spine[i], spine[i + 1] if not (isinstance(a, dict) and isinstance(b, dict)): continue aid = a.get("id") excls = excl_after.get(aid) if not excls: # only a DECLARED exclusion link is a cascade step continue a_n, b_n = counts.get(aid), counts.get(b.get("id")) if a_n is None or b_n is None or any(x is None for x in excls): continue # never guess a missing count got = a_n - sum(excls) if got != b_n: findings.append({ "after": aid, "next": b.get("id"), "detail": (f"STROBE cascade does not close after '{aid}': {a_n:,} - " f"{'+'.join(f'{x:,}' for x in excls)} = {got:,}, but the next box " f"'{b.get('id')}' says {b_n:,} (off by {b_n - got:+,})"), }) return findings def _load(path: Path) -> dict: text = path.read_text(encoding="utf-8") if path.suffix.lower() in {".yaml", ".yml"}: try: import yaml # noqa: PLC0415 except ModuleNotFoundError: sys.exit("PyYAML not installed; install it or pass a JSON config.") return yaml.safe_load(text) or {} import json # noqa: PLC0415 return json.loads(text) def main() -> int: ap = argparse.ArgumentParser(description="STROBE flow cascade-closure check.") ap.add_argument("--config", required=True, help="build_strobe_template.py YAML/JSON config") ap.add_argument("--strict", action="store_true", help="exit 1 if the cascade does not close") a = ap.parse_args() findings = check_cascade(_load(Path(a.config))) if findings: for f in findings: print(f"CASCADE_IMBALANCE: {f['detail']}") else: print("OK: STROBE exclusion cascade closes at every declared link.") return 1 if (findings and a.strict) else 0 if __name__ == "__main__": sys.exit(main())
-
-
templates
-
official
-
consort2010
-
CONSORT_2025_editable_checklist.docx 36.1 KB · in bundle
-
CONSORT_2025_flow_diagram.docx 66.5 KB · in bundle
-
-
prisma2020
-
PRISMA_2020_flow_new_v1.pptx 29 KB · in bundle
-
PRISMA_2020_flow_new_v2.pptx 29.1 KB · in bundle
-
PRISMA_2020_flow_updated_v2.pptx 29.1 KB · in bundle
-
-
spirit2013
-
SPIRIT_2025_editable_checklist.docx 33 KB · in bundle
-
SPIRIT_2025_participant_timeline.docx 19.6 KB · in bundle
-
-
stard2015
-
STARD_2015_checklist.docx 29.1 KB · in bundle
-
STARD_2015_flow_diagram.pdf 311.4 KB · in bundle
-
-
NOTES.md 2.8 KB
# Official Reporting Guideline Templates This directory ships canonical flow diagram and figure templates for the four most commonly required reporting guidelines in clinical research: | Guideline | Source | What we ship | Why | |-----------|--------|--------------|-----| | PRISMA 2020 | prismastatement.org (CC-BY 4.0) | Locally built `.pptx` (4 variants: new/updated × v1/v2) | Site uses Squarespace JS-fingerprint redirect that blocks programmatic download. We reproduce the published layout (Page MJ et al. *BMJ* 2021;372:n71, Fig 1) and supply a `fill_prisma_template.py` to populate counts. | | CONSORT 2025 | consort-spirit.org | Official `.docx` (flow diagram + editable checklist) | Direct fetch works; supersedes CONSORT 2010 per the SPIRIT-CONSORT 2025 update. | | STARD 2015 | equator-network.org | Official `.pdf` flow diagram + `.docx` checklist | Flow diagram is published as PDF only; no Word source exists upstream. | | SPIRIT 2025 | consort-spirit.org | Official `.docx` (participant timeline + editable checklist) | Direct fetch works; supersedes SPIRIT 2013 per the 2025 update. | ## Refresh / verify ```bash bash scripts/fetch_official_templates.sh # all targets bash scripts/fetch_official_templates.sh consort2010 FORCE=1 bash scripts/fetch_official_templates.sh # ignore cache ``` The fetcher reports per-target OK/SKIP/FAIL. URLs are versioned in the script header — if a target rotates, update `TARGETS=` and rerun. ## PRISMA 2020 build/fill workflow ```bash # (one-time) generate template variants python3 scripts/build_prisma2020_template.py --variant new \ --out templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx python3 scripts/build_prisma2020_template.py --variant new --include-other-sources \ --out templates/official/prisma2020/PRISMA_2020_flow_new_v2.pptx # fill with study counts (positional 10-tuple) python3 scripts/fill_prisma_template.py \ --template templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \ --counts "315,122,186,7,111,204,102,84,3,15" \ --out fig1_prisma_filled.pptx # or with a JSON file giving every key python3 scripts/fill_prisma_template.py \ --template templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \ --counts-file my_counts.json \ --out fig1_prisma_filled.pptx ``` Render to PDF/PNG via LibreOffice headless: ```bash soffice --headless --convert-to pdf fig1_prisma_filled.pptx soffice --headless --convert-to png fig1_prisma_filled.pptx ``` ## Attribution PRISMA layout: Page MJ, McKenzie JE, Bossuyt PM, et al. The PRISMA 2020 statement: an updated guideline for reporting systematic reviews. *BMJ* 2021;372:n71. doi:10.1136/bmj.n71. CC-BY 4.0. CONSORT 2025 / SPIRIT 2025: see https://www.consort-spirit.org/. STARD 2015: Bossuyt PM, Reitsma JB, Bruns DE, et al. STARD 2015. *BMJ* 2015;351:h5527.
-
-
-
tests
-
fixtures
-
figure1_flow.yaml 242 B
# STROBE flow config (SSOT) consumed by generate_flow_diagram.R boxes: - id: assessed label: "Assessed for eligibility (n = 1284)" - id: excluded label: "Excluded (n = 286)" - id: analytic label: "Analytic cohort (n = 998)" -
manuscript_ok.md 230 B
## Results The analytic cohort comprised 998 participants. ## Figure Legends **Figure 1.** STROBE flow diagram. Of n = 1,284 assessed for eligibility, n = 286 were excluded, leaving an analytic cohort of n = 998. ## References -
manuscript_stale.md 157 B
## Figure Legends **Figure 1.** STROBE flow diagram. Of n = 1,284 assessed for eligibility, n = 286 were excluded, leaving an analytic cohort of n = 1,150. -
strobe_cascade_balanced.yaml 346 B
# The same cascade, closing exactly: 10,000 - 500 = 9,500. title: "Figure 1. STROBE participant flow diagram" spine: - {id: enrolled, text: "Participants enrolled (n = 10,000)"} - {id: analyzed, text: "Included in the primary analysis (n = 9,500)"} exclusions: - {after: enrolled, text: "Excluded (n = 500):\n- missing baseline covariate"} -
strobe_cascade_branching.yaml 519 B
# A branching Analysis leaf: primary and landmark share the eligible parent, with no # exclusion declared between them. The landmark subset (4,200) must NOT be read as an # unbalanced cascade step off the primary (5,000). title: "Figure 1. STROBE participant flow diagram" spine: - {id: eligible, text: "Eligible cohort (n = 5,000)"} - {id: primary, text: "Primary Cox model (n = 5,000)"} - {id: landmark, text: "Landmark subset analysis (n = 4,200)"} exclusions: - {after: eligible, text: "Excluded (n = 0)"} -
strobe_cascade_imbalanced.yaml 648 B
# Synthetic, PII-free. One exclusion box drops out of the arithmetic: enrolled 10,000 minus # the single declared exclusion 500 is 9,500, but the analysis box reads 9,470 — off by 30 # (a second exclusion, present in the legend, was dropped from the figure). This is the # real-world class: a cascade that is short of the analytic N by one omitted exclusion. title: "Figure 1. STROBE participant flow diagram" spine: - {id: enrolled, text: "Participants enrolled (n = 10,000)"} - {id: analyzed, text: "Included in the primary analysis (N = 9,470)"} exclusions: - {after: enrolled, text: "Excluded (n = 500):\n- missing baseline covariate"}
-
-
test_legend_reconcile.sh 1.5 KB
#!/usr/bin/env bash # Regression test for the Figure 1 caption ↔ flow-SSOT reconciler. # Synthetic fixtures: a flow config with counts {1284, 286, 998}; an OK caption # that matches, and a stale caption citing 1,150 (absent from the diagram). # Stdlib-only (python3). set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT="$HERE/../scripts/derive_figure_legend_counts.py" FLOW="$HERE/fixtures/figure1_flow.yaml" OK="$HERE/fixtures/manuscript_ok.md" STALE="$HERE/fixtures/manuscript_stale.md" OUT="$(mktemp -t fl_XXXX).json" trap 'rm -f "$OUT"' EXIT fail=0 check() { local label="$1"; shift if "$@" >/dev/null 2>&1; then printf ' PASS %s\n' "$label" else printf ' FAIL %s\n' "$label"; fail=$((fail+1)); fi } [[ -f "$SCRIPT" ]] || { echo "ENV-ERR: script missing" >&2; exit 2; } python3 "$SCRIPT" --flow-config "$FLOW" --manuscript "$OK" --strict >/dev/null 2>&1 check "exit 0 when caption matches the flow SSOT" test "$?" -eq 0 python3 "$SCRIPT" --flow-config "$FLOW" --manuscript "$STALE" --out "$OUT" --strict >/dev/null 2>&1 check "exit 1 when caption cites a count absent from the flow SSOT" test "$?" -eq 1 check "stale count 1150 flagged" python3 -c " import json; d=json.load(open('$OUT')) assert 1150 in d['stale_in_caption'], d['stale_in_caption']" check "verdict MISMATCH" python3 -c " import json; assert json.load(open('$OUT'))['verdict']=='MISMATCH'" echo "fail=$fail"; [[ "$fail" -eq 0 ]] && echo "ALL PASS" || echo "FAILURES: $fail" exit "$fail" -
test_pptx_mac_compat.py 4.9 KB
#!/usr/bin/env python3 """Regression test for scripts/validate_pptx_mac_compat.py. Builds a clean .pptx (python-pptx, with a corrected docProps/app.xml slide count) that must PASS, then injects each of the four Mac-incompatibility defect classes into a copy and asserts the validator FAILs under --strict: TIFF media, <a:sp3d> 3-D bevel, app.xml slide-count mismatch, srcRect over-crop (> 100000). Also asserts a missing input exits 2. Requires python-pptx (a CI dependency). """ from __future__ import annotations import re import subprocess import sys import tempfile import zipfile from pathlib import Path HERE = Path(__file__).resolve().parent VALIDATOR = HERE.parent / "scripts" / "validate_pptx_mac_compat.py" _pass = 0 _fail = 0 def ck(label: str, expected: int, actual: int) -> None: global _pass, _fail if expected == actual: print(f" PASS {label:<48} exit={actual}") _pass += 1 else: print(f" FAIL {label:<48} expected={expected} actual={actual}") _fail += 1 def run(pptx: Path, strict: bool = True) -> int: cmd = [sys.executable, str(VALIDATOR), str(pptx)] if strict: cmd.append("--strict") return subprocess.run(cmd, capture_output=True, text=True).returncode def build_clean(path: Path, n_slides: int = 2) -> None: """A python-pptx deck with app.xml <Slides> corrected to match — should PASS.""" from pptx import Presentation prs = Presentation() for _ in range(n_slides): prs.slides.add_slide(prs.slide_layouts[6]) # blank prs.save(str(path)) # python-pptx writes <Slides>0</Slides>; fix it so the clean baseline passes. _rewrite_member(path, "docProps/app.xml", lambda b: re.sub(rb"<Slides>\d+</Slides>", f"<Slides>{n_slides}</Slides>".encode(), b)) def _read_member(path: Path, member: str) -> bytes: with zipfile.ZipFile(path) as z: return z.read(member) def _rewrite_member(path: Path, member: str, transform, extra: dict | None = None) -> None: """Rewrite the zip, replacing `member` (via transform) and adding `extra` files.""" with zipfile.ZipFile(path) as z: items = {n: z.read(n) for n in z.namelist()} if member in items: items[member] = transform(items[member]) if extra: items.update(extra) tmp = path.with_suffix(".tmp.pptx") with zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as z: for n, data in items.items(): z.writestr(n, data) tmp.replace(path) def first_slide(path: Path) -> str: with zipfile.ZipFile(path) as z: for n in sorted(z.namelist()): if n.startswith("ppt/slides/slide") and n.endswith(".xml"): return n raise AssertionError("no slide XML in fixture") def main() -> int: if VALIDATOR.exists() is False: print(f"validator missing: {VALIDATOR}") return 1 tmp = Path(tempfile.mkdtemp()) clean = tmp / "clean.pptx" build_clean(clean) ck("clean deck (app.xml fixed) passes", 0, run(clean)) # 1) TIFF embedded in ppt/media/ tiff = tmp / "tiff.pptx" build_clean(tiff) _rewrite_member(tiff, "docProps/app.xml", lambda b: b, extra={"ppt/media/image1.tiff": b"II*\x00 fake tiff"}) ck("TIFF media -> FAIL", 1, run(tiff)) # 2) <a:sp3d> 3-D bevel inside a slide sp3d = tmp / "sp3d.pptx" build_clean(sp3d) sl = first_slide(sp3d) _rewrite_member(sp3d, sl, lambda b: b.replace(b"</p:sld>", b"<a:sp3d/></p:sld>")) ck("sp3d bevel -> FAIL", 1, run(sp3d)) # 3) app.xml slide-count mismatch (declare a wrong count) appx = tmp / "appx.pptx" build_clean(appx) _rewrite_member(appx, "docProps/app.xml", lambda b: re.sub(rb"<Slides>\d+</Slides>", b"<Slides>99</Slides>", b)) ck("app.xml slide-count mismatch -> FAIL", 1, run(appx)) # 4) srcRect over-crop (> 100000) src = tmp / "srcrect.pptx" build_clean(src) sl2 = first_slide(src) _rewrite_member(src, sl2, lambda b: b.replace(b"</p:sld>", b'<a:srcRect l="997171"/></p:sld>')) ck("srcRect over-crop (>100000) -> FAIL", 1, run(src)) # 5) missing input -> exit 2 ck("missing input -> exit 2", 2, run(tmp / "does_not_exist.pptx")) # 6) non-strict tolerates a WARN-only deck (no app.xml -> WARN, exit 0) nowarn = tmp / "noappxml.pptx" build_clean(nowarn) _rewrite_member(nowarn, "docProps/app.xml", lambda b: b, extra=None) # remove app.xml entirely with zipfile.ZipFile(nowarn) as z: items = {n: z.read(n) for n in z.namelist() if n != "docProps/app.xml"} with zipfile.ZipFile(nowarn, "w", zipfile.ZIP_DEFLATED) as z: for n, data in items.items(): z.writestr(n, data) ck("missing app.xml is WARN, tolerated without --strict", 0, run(nowarn, strict=False)) print("----") print(f"test_pptx_mac_compat: {_pass} passed, {_fail} failed") return 0 if _fail == 0 else 1 if __name__ == "__main__": sys.exit(main()) -
test_strobe_cascade.sh 3 KB
#!/usr/bin/env bash # Regression test for the STROBE flow cascade-closure check (make-figures). # Synthetic, PII-free YAML fixtures modelled on a real cohort-figure defect: # imbalanced -> 10,000 - 500 = 9,500, but the analysis box says 9,470 (off by 30) # balanced -> the same cascade closing exactly (9,500) # branching -> a landmark-subset leaf with no exclusion between it and its parent, which # must NOT be read as an unbalanced cascade step (low-false-positive guard) # The helper (_strobe_cascade.py) is checked directly (no python-pptx needed); the # build_strobe_template.py --strict-cascade integration is checked only when pptx is present. set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CHK="$HERE/../scripts/_strobe_cascade.py" BUILD="$HERE/../scripts/build_strobe_template.py" FX="$HERE/fixtures" fail=0 ck() { if [ "$2" = "$3" ]; then printf ' PASS %s\n' "$1"; else printf ' FAIL %s (want %s got %s)\n' "$1" "$2" "$3"; fail=$((fail+1)); fi; } [ -f "$CHK" ] || { echo "ENV-ERR: helper missing" >&2; exit 2; } # (1) imbalanced cascade -> exit 1 under --strict, and the message names the offending link. out="$(python3 "$CHK" --config "$FX/strobe_cascade_imbalanced.yaml" --strict 2>&1)"; rc=$? ck "imbalanced exits 1 under --strict" 1 "$rc" printf '%s\n' "$out" | grep -q 'CASCADE_IMBALANCE' && ck "imbalanced reports CASCADE_IMBALANCE" yes yes || ck "imbalanced reports CASCADE_IMBALANCE" yes no printf '%s\n' "$out" | grep -q "off by -30" && ck "imbalanced names the -30 offset" yes yes || ck "imbalanced names the -30 offset" yes no # (2) balanced cascade -> exit 0, no imbalance. python3 "$CHK" --config "$FX/strobe_cascade_balanced.yaml" --strict >/dev/null 2>&1; ck "balanced exits 0" 0 "$?" # (3) branching leaf -> exit 0 (the 4,200 subset is not a cascade step off the 5,000 parent). out3="$(python3 "$CHK" --config "$FX/strobe_cascade_branching.yaml" --strict 2>&1)"; ck "branching leaf exits 0 (no false positive)" 0 "$?" printf '%s\n' "$out3" | grep -q 'CASCADE_IMBALANCE' && { echo " FAIL branching leaf falsely flagged" >&2; fail=$((fail+1)); } || printf ' PASS branching leaf not flagged\n' # (4) build integration (only if python-pptx is installed): --strict-cascade refuses the # imbalanced config and builds the balanced one. if python3 -c "import pptx" 2>/dev/null; then tmp="$(mktemp -d)"; trap 'rm -rf "$tmp"' EXIT python3 "$BUILD" --config "$FX/strobe_cascade_imbalanced.yaml" --out "$tmp/x.pptx" --strict-cascade >/dev/null 2>&1 ck "build --strict-cascade refuses the imbalanced config" 1 "$?" python3 "$BUILD" --config "$FX/strobe_cascade_balanced.yaml" --out "$tmp/ok.pptx" --strict-cascade >/dev/null 2>&1 ck "build --strict-cascade builds the balanced config" 0 "$?" [ -f "$tmp/ok.pptx" ] && ck "balanced build wrote the pptx" yes yes || ck "balanced build wrote the pptx" yes no else echo " SKIP build integration (python-pptx not installed)" fi echo "fail=$fail"; [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "FAILURES: $fail" exit "$fail"
-
-
SKILL.md 46.7 KB
--- name: make-figures description: Generate publication-ready figures and visual abstracts for medical research papers. Supports ROC curves, forest plots, CONSORT/STARD/PRISMA flow diagrams, calibration plots, Kaplan-Meier curves, Bland-Altman plots, confusion matrices, pipeline diagrams, and journal-specific visual/graphical abstracts (python-pptx template-based). triggers: figure, plot, graph, diagram, ROC curve, forest plot, flow diagram, CONSORT diagram, PRISMA flow, visualization, chart, visual abstract, graphical abstract, key message, figure design, figure planning, effective figure, cognitive load tools: Read, Write, Edit, Bash, Grep, Glob model: inherit --- # Make-Figures Skill You are helping a medical researcher generate publication-ready figures for medical research manuscripts. Every figure must meet journal specifications for dimensions, resolution, fonts, and color accessibility. Produce clean, data-focused visuals with no chartjunk. ## Credits The Critic Loop (Step 4b) in this skill is inspired by PaperBanana (Zhu et al., *Automating Academic Illustration for AI Scientists*, arXiv:2601.23265, 2025) and by prior self-refinement research — Self-Refine (Madaan et al., 2023), Reflexion (Shinn et al., 2023), and Constitutional AI (Anthropic, 2022). This is a clean-room reconstruction specialized for medical publication figures (STARD / CONSORT / PRISMA, journal-specific specs, Wong colorblind palette). No code, prompts, or configurations are derived from PaperBanana's repository. ## Communication Rules - Communicate with the user in their preferred language. - All figure text (labels, legends, annotations) must be in English. - Medical terminology is always in English. ## Data Privacy Check Before reading any data file, check whether it might contain Protected Health Information (PHI): 1. If `*_deidentified.*` files exist in the working directory, use those preferentially. 2. If only raw CSV/Excel files exist (no `*_deidentified.*` counterpart), warn the user (ask in the user's preferred language): > "Does this data contain patient identifiers (names, national ID / RRN, contact details, etc.)? > If so, please de-identify it first with the `/deidentify` skill." 3. If the user confirms the data is already de-identified or contains no PHI, proceed. ## Reference Files - **Figure specifications**: `${CLAUDE_SKILL_DIR}/references/figure_specs.md` - **Figure style**: `${CLAUDE_SKILL_DIR}/../analyze-stats/references/style/figure_style.mplstyle` (or project's CLAUDE.md if available) - **Project data**: See CLAUDE.md for data locations under `2_Data/` Read `figure_specs.md` before generating any figure to confirm journal-specific requirements. --- ## Journal AI-Image Policies (CRITICAL — check BEFORE generation) > Synced with the user's global rule `~/.claude/rules/journal-ai-image-policies.md`. The table below is the local copy used during autonomous workflow; the global rule is authoritative when conflicts arise. | Journal family | Policy on AI-generated images | Disclosure required | |---|---|---| | **JACC family (incl. JACC: Asia, JACC Imaging, JACC EP, JACC BTS)** | **Prohibited without prior Editor-in-Chief permission** ([JACC pathway, PMC10167500](https://pmc.ncbi.nlm.nih.gov/articles/PMC10167500/)) | Cover-letter pre-submission inquiry + ICMJE-style declaration | | NEJM | AI image generation prohibited | N/A | | Radiology / Radiology AI | Allowed with disclosure | Manuscript disclosure block | | Nature family | Allowed with disclosure + license check | Methods + figure legend | | Lancet family | Disclosure required, generation discouraged | Manuscript disclosure | | Default (target unknown) | Treat as prohibited until confirmed | N/A | **Hard rule**: For JACC, NEJM, or any "unknown" target journal, **never** use Gemini / DALL-E / Midjourney / Stable Diffusion / Nano Banana to create images that will appear in figures, Central Illustrations, or graphical abstracts. AI text-editing of the manuscript prose remains acceptable subject to standard disclosure. ### Default workflow when AI images are not allowed 1. **SMART Servier Medical Art** — https://smart.servier.com/, CC BY 4.0, free, 3,000+ vector medical icons (anatomy, organs, ethnicity-specific human figures, drugs, devices). Commercial / journal use allowed. **Required attribution** (1 line in figure legend OR methods): > Anatomical icons modified from SMART Servier Medical Art (CC BY 4.0). 2. **NIAID BioArt** (https://bioart.niaid.nih.gov) — public domain (US Govt), microbiology / immunology / lab-tech focus. 3. **BioRender** (https://www.biorender.com) — institutional license usually required; use the exported "Publication-ready" PNG/TIFF and cite per BioRender publication policy. 4. For "diseased" variants not directly available (e.g., calcified vessel from a clean vessel): reuse the healthy asset and overlay disease markers via matplotlib `scatter` / `Circle` / `PathPatch`. Keeps the entire pipeline non-AI and reproducible. ### Asset directory convention ``` manuscript/figures/_assets_servier/ # CC BY 4.0 source PNGs manuscript/figures/_assets_servier/CITATION.md # source URL + download date per asset manuscript/figures/_assets_data/ # data-driven raster (R / matplotlib heat maps, KM, etc.) manuscript/figures/_legacy/ # archived prior versions ``` Composition scripts should load only from `_assets_servier/` and `_assets_data/`. If a script imports from `_assets_ai/`, treat it as a policy violation for JACC/NEJM/unknown targets. When a figure is produced by a data-driven `.py`/`.R` script (ROC, forest, KM, calibration, heat maps), lint that script before finalizing with the `/analyze-stats` code-quality gate (`check_generated_code.py {script} --strict`): it catches a missing plotting seed for any bootstrapped CI band, a hardcoded absolute data path, or a hand-typed data literal that should have been read from the analysis CSV. ### Decoration vs information Even when AI images are allowed, AI-generated illustrations are immediately recognizable to experienced reviewers (small decorative icons that add no information, overly uniform layouts, generic clip-art style). For high-impact submissions, prefer Servier / BioArt / BioRender + matplotlib overlays over AI. --- ## DPI and Resolution Guide | Output | Minimum DPI | Notes | |--------|------------|-------| | Journal halftone (photos, screenshots) | 300 | Standard for most journals | | Journal line art (diagrams, graphs) | 600 | Required by Radiology, most Elsevier journals | | Poster presentation | 150-200 | Lower is acceptable for large-format prints | | Screen/web only | 72-150 | Not for print submission | **Practical workflow for screen captures**: - Use HyperSnap or similar tool with DPI pre-set to the journal requirement - Compose the figure in PPT at high zoom → capture at target DPI → save as TIFF/PNG - Verify final file dimensions match journal column width requirements --- ## Visual Abstract / Graphical Abstract Many journals now require or strongly encourage visual abstracts. European Radiology made graphical abstracts mandatory for all Original Articles from first revision (Jan 2025). Submitting one voluntarily signals effort and can improve editorial impression. ### Journal Requirements | Status | Example Journals | |--------|-----------------| | **Mandatory** | European Radiology (from 1st revision, all Original Articles) | | **Encouraged** | Abdominal Radiology, JCO, Annals of Internal Medicine | | **Voluntary** | Most other journals — improves social media visibility | Check the target journal profile (`write-paper/references/journal_profiles/`) for specific visual abstract requirements before starting. ### Workflow 1. **Check journal template.** Look for an official PPTX template in `${CLAUDE_SKILL_DIR}/references/visual_abstract_templates/{journal}.pptx`. If no journal-specific template exists, use `medsci_default.pptx`. 2. **Extract content from the manuscript:** - **Title:** Full article title - **Hypothesis/Question:** Derived from Key Point 1 or study objective (max 1 sentence) - **Methodology:** Brief flowchart or ≤3 bullets, <6 words each - **Visual element:** Study's own figure (ROC curve, flow diagram, representative image) - **Badges:** Patient cohort (N=...) | Modality/organ | Single/Multi-center - **Main finding:** Derived from Key Point 3 (<20 words) - **Citation:** Journal (year) Authors; DOI 3. **Select visual element** (priority order — no API needed for top options): 1. Study's own figures (ROC, flow diagram, representative image) — **always preferred** 2. Free illustration from Servier Medical Art or NIAID BioArt (see `${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md`) 3. Manual drawing in PPT/Keynote/Figma 4. AI generation via `generate_image.py --style medical` (only if GEMINI_API_KEY set) 4. **Generate using the script:** ```bash python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \ --template medsci_default \ --title "Article Title" \ --hypothesis "Research question" \ --methods "Method 1|Method 2|Method 3" \ --finding "Main finding statement" \ --citation "Eur Radiol (2026) Author A et al; DOI:..." \ --visual figures/fig1_roc_curve.png \ --badges "N=450|CT chest|Multi-center" \ --output figures/visual_abstract.pptx ``` 5. **Review with user.** Open the PPTX to verify layout and content. Iterate. 6. **Export.** PPTX is the primary deliverable. For PNG: open in PowerPoint/Keynote → export, or use LibreOffice CLI (`soffice --headless --convert-to png`). ### Design Principles - One page, landscape (16:9) or per journal template specification - Three sections: Study question → Key method → Main result - Use the study's actual figures rather than generic graphics - Minimize text — let visuals carry the message - Every visual element must serve a purpose (no decorative clip-art) ### Available Templates | Template | File | Use When | |----------|------|----------| | MedSci Default | `medsci_default.pptx` | Any journal without an official template | | JACC Central Illustration | `jacc_central_illustration.pptx` | JACC family journals (use `--type central-illustration`) | **Using a journal's own template.** Several journals publish one — European Radiology requires a graphical abstract from first revision and supplies `EURA-GA-Jan2025.pptx`. We do not redistribute them: a template you may download is not a template we may ship. Use yours directly instead: ```bash python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \ --template /absolute/path/to/EURA-GA-Jan2025.pptx ... ``` `--template` takes an absolute path to any `.pptx`. The script locates the fields by their text content rather than by shape name, so a journal's own template works unmodified. If the path does not exist it falls back to `medsci_default.pptx`. To add a new journal template: see `${CLAUDE_SKILL_DIR}/references/visual_abstract_templates/template_guide.md`. --- ## Central Illustration vs Visual Abstract A Central Illustration (CI) is **not** a Visual Abstract (VA). They serve different purposes and follow different rules. JACC family journals (JACC, JACC: Asia, JACC: Cardiovascular Imaging, JACC: Heart Failure, JACC: CardioOncology, JACC: Clinical Electrophysiology, JACC: Basic to Translational Science) require a Central Illustration with every Original Article. Reference: Fuster V, Mann DL. *JACC.* 2019;74(22):2816–2820. | Aspect | Central Illustration | Visual Abstract | |---|---|---| | Purpose | Single key finding / take-home message | Methods + Results pictorial summary | | Where in paper | End of Results / start of Discussion | Beginning of paper | | Methods content | **None** | Required | | Audience | Cardiovascular clinicians + journal-issue readers | Broad including non-specialists / social media | | Used by | All JACC family + JACC: Asia | Originally JACC: Basic to Translational Science | | Text density | Minimal (graphical priority) | More allowed (methods labels) | | Bar graphs | OK if they capture entire message | Avoid — use ↑↓ arrows | | Default complexity | 1–3 visual zones | Q→M→R three blocks | ### Fuster-Mann five rules (CI must pass all) 1. **Know the message.** One finding, not study design + multiple findings. 2. **Convey graphically, not textually.** Even a simple KM curve is OK. 3. **Avoid using too much text.** Replace with icons or arrows. 4. **Avoid secondary messages.** ≤ 5 seconds for a viewer to state the main finding. 5. **Simplicity is superior.** Default to fewer panels. Full guidance and validation thresholds: `${CLAUDE_SKILL_DIR}/references/jacc_central_illustration_principles.md`. ### CI mode invocation ```bash python ${CLAUDE_SKILL_DIR}/scripts/generate_visual_abstract.py \ --type central-illustration \ --visual figures/central_illustration_v2.png \ --citation "FirstAuthor Last et al. Journal Name 2026; vol(issue):pages." \ --output submission/jacc_asia/central_illustration.pptx \ --ci-zones 3 --ci-label-words 22 --ci-numerical-points 2 \ --ci-raw-text "warranty drops to 3 years in age 45+ with cardiometabolic burden; MASLD HR 1.77" ``` CI mode validates before rendering and rejects (exit 2) if any of: zones > 3, label words > 30, numerical points > 4, or methodology terms (cohort flow / inclusion / exclusion / study design / enrollment / randomized / sample size / CONSORT / PRISMA / STARD) appear in `--ci-raw-text`. Override individual rules with `--ci-allow {zones|words|numerical|methods}` only when you have a defensible reason. The JACC submission PPTX is a 10×7.5 in slide with 4 placeholders (citation textbox, content picture, footer textbox reserved, JACC logo). The red border + blue "CENTRAL ILLUSTRATION:" header are applied by JACC editorial after acceptance — authors submit only the content figure + citation. --- ## Workflow ### Step 1: Specify **Before specifying figure type, read `${CLAUDE_SKILL_DIR}/references/design_principles.md`** — identify (1) the one-sentence key message, (2) audience and reading-time budget, and (3) whether a figure is the right vehicle (vs a small table or in-line text). The five strategies in that file shift Step 1 from "which chart fits the data" to "what should the reader remember 10 seconds later." Skip only when the figure is mandated by a reporting guideline (e.g., PRISMA / CONSORT flow), and even then apply the cognitive-load checklist. **For reporting-guideline figures**, also load `${CLAUDE_SKILL_DIR}/references/reporting_guideline_figure_map.md` — the 14-row table tells you which guideline mandates which figures and whether this skill ships an official template (✅), generic flow only (⚠️), or needs manual production (❌). Critical for AI-extension guidelines (CONSORT-AI, STARD-AI, TRIPOD+AI, CLAIM 2024, DECIDE-AI). **For medical AI / engineering pipeline figures** (DICOM workflow, annotation pipeline, federated learning topology, model architecture), also load `${CLAUDE_SKILL_DIR}/references/pipeline_concepts_medical_ai.md` — canonical layouts, required annotations, and tool selection per type. **Optional flags:** - `--study-type <type>`: One of: `diagnostic-accuracy`, `ai-validation`, `meta-analysis`, `dta-meta-analysis`, `observational-cohort`, `rct`, `case-report`. When set, auto-generate the full figure set from the Study-Type Figure Sets table below without prompting for individual figure types. - `--data-dir <path>`: Directory containing analysis outputs (CSVs, `_analysis_outputs.md`). Default: current working directory. Ask the user for: 1. **Figure type** (from the supported types below) — skipped when `--study-type` is provided 2. **Data source** (file path, DataFrame, or manual values) 3. **Target journal** (for dimension/font requirements) 4. **Panel layout** (single panel, multi-panel, or let you decide) 5. **Any special requests** (annotations, highlights, reference lines) 6. **Study type** (if not passed via `--study-type`): determines the required figure set If the user provides enough context, infer missing parameters and confirm before proceeding. ### Step 2: Configure 1. Load the figure style file: ```python import matplotlib.pyplot as plt import os style_path = os.path.join(os.environ.get('CLAUDE_SKILL_DIR', '.'), '../analyze-stats/references/style/figure_style.mplstyle') if os.path.exists(style_path): plt.style.use(style_path) ``` 2. Look up journal-specific dimensions from `${CLAUDE_SKILL_DIR}/references/figure_specs.md`. 3. Set the colorblind-safe palette (Wong palette by default). 4. Configure font sizes per element type (title, axis label, tick label, legend, annotation). ### Step 3: Generate Create the figure using Python (matplotlib/seaborn as primary, with specialized libraries as needed). **Script structure:** ```python """ Figure: {description} Date: {YYYY-MM-DD} Target: {journal} Dimensions: {width} x {height} inches @ {DPI} DPI """ import numpy as np import matplotlib.pyplot as plt import os style_path = os.path.join(os.environ.get('CLAUDE_SKILL_DIR', '.'), '../analyze-stats/references/style/figure_style.mplstyle') if os.path.exists(style_path): plt.style.use(style_path) # Wong colorblind-safe palette WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7'] np.random.seed(42) ``` ### Step 4: Review Present the figure to the user and ask: - Does the layout work? - Are labels and annotations correct? - Any adjustments to colors, sizing, or emphasis? Iterate until the user approves. ### Step 4b: Critic Loop (self-critique before final export) Before Step 5 Export, run the automated Critic Loop. This is two stages — deterministic quantitative checks via Python, then qualitative review by Claude itself — and the combined output tells us whether to re-render or hand off to the user. **Stage 1: Quantitative checks (`critic_figure.py`)** ```bash python ${CLAUDE_SKILL_DIR}/scripts/critic_figure.py \ figures/fig1_stard.png \ --type stard \ --spec-min-dpi 600 \ --spec-width-in 7.0 \ --source-text figures/fig1_stard.txt \ # optional: expected strings for OCR coverage --out figures/fig1_stard.critique.json ``` This produces a JSON report covering: - DPI and physical width vs. journal spec - Dominant-color breakdown and out-of-Wong-palette fraction - OCR-detected word count, minimum text height, and (if a source-text file was provided) source-word coverage **Stage 2: Qualitative review (Claude session)** 1. Use the Read tool to load the generated PNG. 2. Read the corresponding rubric file: - Flow diagrams: `${CLAUDE_SKILL_DIR}/references/critic_rubrics/flow_diagram.md` (sections A–G; section G adds cognitive-load and template-fidelity checks) - Data plots: `${CLAUDE_SKILL_DIR}/references/critic_rubrics/data_plot.md` (sections A–G; section G adds calibration / fairness / colorblind+redundant / dataset-flow / decision-curve checks for medical AI papers) - For PRISMA / CONSORT / STARD / STROBE specifically, also read `${CLAUDE_SKILL_DIR}/references/flow_diagram_lessons.md` — five production lessons covering official-template fidelity, PDF export fidelity (VML fallback), docx XML escape, sequential placeholder mapping, and frozen-version sync with the manuscript. - For AI-extension guidelines (CONSORT-AI, STARD-AI, TRIPOD+AI, CLAIM 2024, DECIDE-AI), also read `${CLAUDE_SKILL_DIR}/references/reporting_guideline_figure_map.md` — the row for the target guideline lists mandatory figures and which ones this skill cannot template (production path documented per row). - For medical-AI pipeline / DICOM / federated / architecture figures, also read `${CLAUDE_SKILL_DIR}/references/pipeline_concepts_medical_ai.md`. 3. Read the `_why.md` design notes in `${CLAUDE_SKILL_DIR}/references/exemplar_diagrams/{type}/` — hierarchy, whitespace, typography, emphasis, colour. **They are the anchors.** Where a rendered exemplar is bundled (`template_output*.png`, produced by this skill's own R script), Read 1–2 of those too; the figures cropped from published papers were removed in 2026-07 because an MIT-licensed package cannot redistribute them (see that directory's README). If you have your own exemplars locally, point the loop at them — they stay on your machine. For a non-flow data plot (forest, ROC, KM, calibration), read the matching anatomy model in `${CLAUDE_SKILL_DIR}/references/exemplar_plots/` (e.g., `forest_plot.md`). 4. Score every rubric item as PASS / PARTIAL / FAIL with a one-line note, using the format at the bottom of the rubric file. 5. Emit a **"Required edits before next render"** list of concrete source-code changes (D2 node renames, count corrections, matplotlib parameter tweaks). **Refinement loop** - If all items are PASS → proceed to Step 5 Export with `critic_pass: yes`. - If any item is FAIL → apply the required edits to the source (D2 file or matplotlib script), re-render, and re-run Stage 1 + Stage 2. Default maximum is **T=2 rounds**; the user may request up to T=3. - If after the max rounds some items remain PARTIAL, proceed with `critic_pass: partial` and record the residual items in the manifest's `critic_notes` field. Record the final state in `_figure_manifest.md` (see the manifest format below) so downstream steps (`/write-paper` Phase 2 embedding and Phase 7 DOCX build) and future critic passes can see the history. ### Step 5: Export Save final outputs: - **PDF** (vector format, preferred for journal submission) - **PNG** (300 DPI raster, for review and presentation) - **TIFF** (if the journal requires it, 300 DPI LZW compression) Name files descriptively: `fig1_roc_curve.pdf`, `fig2_consort_flow.pdf`, etc. **For PPTX outputs (visual abstract, central illustration, or any deck the figure will live in)**: run the Mac-compatibility validator before delivery. PowerPoint Mac silently drops TIFF, renders `<a:sp3d>` 3-D bevels as red outlines that PDF export does not show, and refuses to open files whose `app.xml` slide count disagrees with the actual slide XML files. This script catches all four classes of defect codified in `~/.claude/rules/pptx-mac-compatibility.md`: ```bash python ${CLAUDE_SKILL_DIR}/scripts/validate_pptx_mac_compat.py \ figures/visual_abstract.pptx \ --json figures/visual_abstract.mac_compat.json \ --strict ``` Exit code 1 means at least one FAIL — fix per the `fix:` field in the JSON report and re-render the PPTX before delivery. Exit code 0 with WARN is acceptable. Skip this step when the figure is PNG/PDF only (no PPTX). ### Step 6: Design QC Checklist Before delivering the final figure, verify all items: - [ ] **Font**: Sans-serif (Arial/Helvetica), minimum 7pt, axis labels ≥ 9pt - [ ] **Color**: Wong/Okabe-Ito colorblind-safe palette used - [ ] **Colorblind test**: Would the figure work for deuteranopia? (no red-green only distinctions) - [ ] **Grayscale test**: Information preserved when printed in black & white - [ ] **Alignment**: All elements on a consistent grid; panels aligned - [ ] **Vector output**: PDF/SVG saved (not just PNG) - [ ] **Resolution**: ≥ 300 DPI for raster elements, ≥ 600 DPI for line art - [ ] **Journal specs**: Dimensions, font, and format match target journal requirements - [ ] **No chartjunk**: No 3D effects, unnecessary gridlines, gradient fills, or decorative elements - [ ] **Caption**: Drafted with key finding, abbreviations, statistical details, and sample size --- ## Study-Type Figure Sets When the study type is known (from `/write-paper` Phase 0 or user specification), auto-detect and generate the complete required figure set without asking for each figure individually. | Study Type (Guideline) | Required Figures | |---|---| | Diagnostic accuracy (STARD) | STARD flow diagram, ROC curve, confusion matrix, calibration plot | | AI validation (TRIPOD+AI / CLAIM) | Flow diagram, ROC curve, confusion matrix, calibration plot, feature importance or SHAP, Grad-CAM (if imaging) | | Meta-analysis (PRISMA) | PRISMA flow diagram, forest plot, funnel plot | | DTA meta-analysis (PRISMA-DTA) | PRISMA flow diagram, paired forest plot (Se + Sp), SROC curve, Deeks funnel plot | | Observational cohort (STROBE) | Flow diagram, Kaplan-Meier curves (if survival endpoint) | | RCT (CONSORT) | CONSORT flow diagram, primary endpoint figure | | Case report / series (CARE) | Clinical timeline figure (`exemplar_plots/clinical_timeline.md`), annotated multimodality imaging panel when visually load-bearing (`exemplar_plots/imaging_panel.md`); for a series, an all-cases summary table | **The manifest is mandatory.** After generating all figures, write `figures/_figure_manifest.md` — one row per figure (`Figure | Path | Type | Tool | Critic | Rounds | Description`) plus a `## Critic notes` section recording any residual PARTIAL items and why they were accepted. It is consumed by `/write-paper` Phase 2 (figure embedding) and Phase 7 (DOCX build); verify it exists and is non-empty before finishing. Format and field definitions: `${CLAUDE_SKILL_DIR}/references/figure_manifest.md`. **Flow diagram generation rule.** STARD / CONSORT / PRISMA / STROBE flow diagrams **MUST** use the standardized R pipeline `scripts/generate_flow_diagram.R` (DiagrammeR + Graphviz dot + rsvg) — the single canonical tool for all four. Do **NOT** use matplotlib `FancyBboxPatch` (manual coordinates break when text changes, and patches distort when embedded in DOCX). Do **NOT** use D2 for new flow diagrams (weak font control, overlap needs manual post-processing). Numbers in labels must be CSV-derived, or hand-written only when the value lives in a commit-tracked data artifact. **Read on demand:** | File | Read it when | Cost if read blindly | |---|---|---| | `references/flow_diagram_recipe.md` | you are generating a STARD / CONSORT / PRISMA / STROBE flow diagram | ~2,200 tokens — a ROC curve or forest plot needs none of it | | `references/figure_manifest.md` | you are writing `_figure_manifest.md` | ~700 tokens of output format | ## Tool Selection Guide Choose the right tool for each figure type. Using matplotlib for flow diagrams leads to hard-coded coordinates that break when text changes — use auto-layout tools instead. ### Data Visualization → matplotlib/seaborn (this skill) Best for figures where data drives the layout. This skill handles these directly: | Type | Use Case | Key Library | |------|----------|-------------| | ROC Curve | Diagnostic accuracy | matplotlib, sklearn | | Forest Plot | Meta-analysis | matplotlib | | Calibration Plot | Prediction model | matplotlib | | KM Curve | Survival analysis | lifelines, matplotlib | | Bland-Altman | Agreement | matplotlib | | Confusion Matrix | Classification | seaborn | | Box/Violin Plot | Group comparison | seaborn | | Bar Chart | Categorical comparison | matplotlib | | Heatmap | Correlation/agreement | seaborn | ### Flow Diagrams → Dedicated Tools (NOT matplotlib) Flow diagrams require auto-layout engines. Do NOT use matplotlib patches with manual coordinates — this causes the "absolute coordinate hell" problem where changing one box breaks all downstream positions. | Type | Recommended Tool | Why | |------|-----------------|-----| | STROBE (cohort / cross-sectional) | **`scripts/generate_flow_diagram.R --type strobe`** | Single canonical tool; auto-layout; vector PDF + 300/600 dpi PNG | | CONSORT (RCT) | **`scripts/generate_flow_diagram.R --type consort`** | Same pipeline; monochrome Arial default | | PRISMA 2020 (SR/MA) | **`scripts/generate_flow_diagram.R --type prisma`** | Faithfully implements PRISMA 2020 structure; avoids PRISMA2020 R package's webshot-based raster PDF issue | | STARD (DTA) | **`scripts/generate_flow_diagram.R --type stard`** | Same pipeline; supports 2x2 reference-standard split | | Pipeline Diagram | **D2** (legacy) | Until pipeline-diagram support is added to the R script | **R workflow for flow diagrams:** See the "R flow diagram recipe" above in the Flow diagram generation rule. Key points: YAML config → `Rscript scripts/generate_flow_diagram.R --type <t> --config <yaml> --out <prefix>` → PDF + 300/600 dpi PNG. Templates in `references/exemplar_diagrams/{strobe,consort,prisma,stard}/template_input.yaml`. ### Official Reporting Guideline Templates → `templates/official/` When a journal requires the canonical, statement-issued template (rather than the auto-laid-out R version), use the bundled official files in `templates/official/{prisma2020,consort2010,stard2015,spirit2013}/`. | Guideline | What ships | When to use | |-----------|-----------|-------------| | PRISMA 2020 | Locally built `.pptx` (4 variants) + `fill_prisma_template.py` | Reviewer asks for the official PRISMA 2020 layout, or you want editable PowerPoint instead of an R-rendered PDF. | | STROBE (cohort) | Parametric `.pptx` builder `build_strobe_template.py` (single-script, takes YAML config) | Cohort/case-control study Figure 1 when co-authors want PowerPoint they can hand-edit. Auto-fits text, content-fits slide, dashed-border exclusion side-branches with strictly-horizontal connectors. Optional left-side phase column (omit `stages:` for the plain STROBE convention; include it for the PRISMA-style Identification/Screening/Inclusion/Analysis column). Pair with `generate_flow_diagram.R --type strobe` for the vector PDF/TIFF submission file. | | CONSORT 2025 | Official `.docx` flow diagram + checklist | RCT submissions to journals that mandate the consort-spirit.org template. | | STARD 2015 | Official `.pdf` flow diagram + `.docx` checklist | Diagnostic accuracy studies; flow diagram is fixed PDF, checklist is editable. | | SPIRIT 2025 | Official `.docx` participant timeline + checklist | Trial protocols. | Refresh / fill workflow: ```bash # Refresh from canonical sources (CC-BY 4.0 / public-statement licenses) bash ${CLAUDE_SKILL_DIR}/scripts/fetch_official_templates.sh # Build PRISMA 2020 .pptx (one-time; site blocks programmatic .docx fetch) python3 ${CLAUDE_SKILL_DIR}/scripts/build_prisma2020_template.py \ --variant new \ --out ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx # Fill counts — positional 10-tuple matching most SR/MA workflows: # n_db, n_dup, n_screened, n_screen_excluded, # n_sought, n_assessed, n_excl_r1, n_excl_r2, n_excl_r3, n_studies python3 ${CLAUDE_SKILL_DIR}/scripts/fill_prisma_template.py \ --template ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \ --counts "315,122,186,7,111,204,102,84,3,15" \ --out fig1_prisma_filled.pptx # Or use full JSON mapping for studies with non-standard PRISMA splits python3 ${CLAUDE_SKILL_DIR}/scripts/fill_prisma_template.py \ --template ${CLAUDE_SKILL_DIR}/templates/official/prisma2020/PRISMA_2020_flow_new_v1.pptx \ --counts-file my_counts.json \ --out fig1_prisma_filled.pptx # STROBE — parametric single-script builder (cohort study; spine structure varies per study). # YAML schema: stages, spine (id/stage/text), exclusions (after/text). Consecutive same-stage # rows share one phase label automatically. Stage box fills auto-pick readable text color. python3 ${CLAUDE_SKILL_DIR}/scripts/build_strobe_template.py \ --config figures/figure1_strobe.yaml \ --out figures/figure1_strobe.pptx ``` The builder checks that the exclusion cascade closes — the count in a spine box, minus the exclusions declared after it, must equal the next spine box (`A - Σ(exclusions after A) == B`), for every link that declares an exclusion. It warns loudly on any imbalance and, with `--strict-cascade`, refuses to build. This catches the figure-image arithmetic drift that text-grep and prose gates miss (a dropped exclusion leaving the figure short of the analytic N). Run `scripts/_strobe_cascade.py --config figure1_strobe.yaml --strict` to check a config without rebuilding the diagram. For STROBE the canonical KJR/Radiology/BMJ submission flow is: 1. Render the vector submission file via the auto-fitting Graphviz path: `Rscript ${CLAUDE_SKILL_DIR}/scripts/generate_flow_diagram.R --type strobe --config figures/figure1_strobe_graphviz.yaml --out figures/figure1` 2. Build the editable PowerPoint companion via `build_strobe_template.py` so co-authors and senior reviewers can adjust prose/positioning before sign-off. 3. Re-export the final PPTX to PDF/TIFF only after co-author edits are integrated. See `templates/official/NOTES.md` for licenses, attribution, and refresh notes. ### Visual / Graphical Abstracts → python-pptx Template Generator | Type | Recommended Tool | |------|-----------------| | Visual Abstract (any journal) | `generate_visual_abstract.py` with PPTX template | | Visual element illustration | Study's own figures (preferred), or free libraries (Servier/NIAID) | | Medical Illustration | See `${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md` | See the Visual Abstract section above for the full workflow. ### Hybrid Workflow (recommended for publication) ``` Data plots: matplotlib/seaborn → PDF + PNG (this skill) Flow diagrams: generate_flow_diagram.R (DiagrammeR + rsvg) → PDF + 300/600 dpi PNG Final assembly: pandoc or python-docx (auto-embedded in DOCX) ``` --- ## Supported Figure Types (matplotlib/seaborn) | Type | Use Case | Key Library | Output | |------|----------|-------------|--------| | ROC Curve | Diagnostic accuracy | matplotlib, sklearn | Single/multi-model ROC with AUC | | Forest Plot | Meta-analysis | matplotlib | Effect sizes with CIs, diamond summary | | Calibration Plot | Prediction model | matplotlib | Observed vs predicted with Hosmer-Lemeshow | | KM Curve | Survival analysis | lifelines, matplotlib | With risk table, log-rank p | | Bland-Altman | Agreement | matplotlib | With mean diff, +/-1.96 SD limits | | Confusion Matrix | Classification | seaborn | Heatmap with percentages | | Box/Violin Plot | Group comparison | seaborn | With individual data points | | Pipeline Diagram | Methods figure | D2 (preferred) or matplotlib | Processing/workflow steps | | Bar Chart | Categorical comparison | matplotlib | With error bars (CI or SD) | | Heatmap | Correlation/agreement | seaborn | Color-coded matrix | --- ## Figure Type Templates ### ROC Curve ```python from sklearn.metrics import roc_curve, auc fig, ax = plt.subplots(figsize=(3.5, 3.5)) fpr, tpr, _ = roc_curve(y_true, y_score) roc_auc = auc(fpr, tpr) ax.plot(fpr, tpr, color=WONG[5], lw=1.5, label=f'Model (AUC = {roc_auc:.3f})') ax.plot([0, 1], [0, 1], 'k--', lw=0.8, alpha=0.5) ax.set(xlabel='1 - Specificity', ylabel='Sensitivity', xlim=[-0.02, 1.02], ylim=[-0.02, 1.02]) ax.legend(loc='lower right', frameon=False) ``` - For multiple models: use distinct Wong palette colors, include AUC + 95% CI in legend. - For comparison: report DeLong p-value in annotation. ### Forest Plot - Horizontal layout: effect sizes as squares (sized by weight), CIs as lines. - Diamond at bottom for pooled estimate. - Vertical dashed line at null effect (OR=1 or MD=0). - Axis label: "Favours A | Favours B" or appropriate. - Include heterogeneity stats (I-squared, p) below the diamond. ### Flow Diagrams (STROBE / CONSORT / PRISMA / STARD) **Single canonical tool: `scripts/generate_flow_diagram.R`** (see the R flow diagram recipe above). Do not fall back to matplotlib for flow diagrams — manual coordinates break when text changes and patches distort in DOCX. D2 remains a documented legacy fallback only when R is unavailable. Layout invariants: - Rectangular boxes with rounded corners for stages; notes (`shape: note`) for exclusion side-boxes. - Vertical top-down flow by default; horizontal only when the manuscript layout demands it. - Every box label contains the count (e.g., `"Assessed for eligibility\n(n = 450)"`). - Numbers are CSV-derived (numerical-safety) — author the YAML from an R/Python script that reads the upstream data, or cite the source file in a comment when a literal value is unavoidable. - Follow the official template layout from each guideline. - **Use relative positioning** — never hard-code absolute y-coordinates. Calculate each box position from the previous box's bottom edge plus a consistent gap constant. - **Define gap constants** at the top of the script (e.g., `GAP_SMALL = 1.5`, `GAP_BRANCH = 2.2`). - **Avoid magic number padding** in arrow endpoints — use named constants. **D2 approach (legacy fallback — use only when R is unavailable; the R script above is canonical):** ```bash d2 --layout elk --theme 0 flow.d2 output.svg # Then: open SVG in Figma → grid-snap → font swap → export PDF ``` **Caption ↔ flow-SSOT reconciliation (before Step 5 Export).** The flow-diagram config (the YAML/script that `generate_flow_diagram.R` consumes) is the single source of truth for participant counts. A hand-written Figure 1 caption drifts from it whenever the cohort is re-locked but the caption is not — the classic "caption says n = 1,284 analytic, diagram box says n = 998" defect, which surfaces only at submission. Re-derive the caption counts from the flow config and reconcile: ```bash python3 ${CLAUDE_SKILL_DIR}/scripts/derive_figure_legend_counts.py \ --flow-config figures/figure1_strobe_graphviz.yaml \ --manuscript manuscript/index.qmd \ --out qc/figure_legend_counts.json --strict ``` Any `n = N` in the caption that is not a box count in the flow config is a `MISMATCH` (stale caption) — update the caption from the config, never the reverse. This pairs with numerical-safety's "re-derive prose counts every revision" rule and with `/sync-submission`'s cross-document N checks. (The reconciler is stdlib-only and parses the config as text, so it works regardless of the flow tool.) ### Calibration Plot - 45-degree reference line (perfect calibration). - Grouped observed vs predicted with error bars. - Report Hosmer-Lemeshow statistic and Brier score in annotation. - Optional: histogram of predicted probabilities at the bottom. ### Kaplan-Meier Curve - Step function with distinct colors per group. - Censoring marks as small vertical ticks. - Number-at-risk table below the plot (aligned with x-axis ticks). - Log-rank p-value in annotation. - Median survival with 95% CI if applicable. ### Bland-Altman Plot - X-axis: mean of two measurements. - Y-axis: difference between measurements. - Horizontal lines: mean difference (solid), +/-1.96 SD (dashed). - Annotate the mean diff and limits of agreement values. - Optional: proportional bias check (regression line through points). ### Confusion Matrix - Heatmap with both counts and percentages in each cell. - Row-normalized percentages preferred (sensitivity per class). - Clear axis labels: "Predicted" (x) and "Actual" (y). - Use sequential colormap (Blues or Greens), not diverging. ### Box/Violin Plot - Show individual data points (jittered) overlaid on box or violin. - Mark median and mean distinctly. - Statistical annotation brackets with significance stars. - Stars: * p<0.05, ** p<0.01, *** p<0.001, ns for non-significant. ### Pipeline Diagram - Horizontal or vertical flow of processing stages. - Boxes: rounded rectangles with stage name and brief description. - Arrows: labeled with data counts or transformation type. - Color-code stages by category (data collection, processing, validation). - Keep text minimal; use supplementary caption for details. ### Bar Chart - Error bars: 95% CI (preferred) or SD, stated in caption. - Individual data points overlaid if n < 30. - Horizontal orientation for many categories. - Sort by value (descending) unless order is meaningful. ### Heatmap - Annotate cells with values. - Use sequential colormap for correlation (coolwarm diverging if centered at zero). - Mask diagonal for correlation matrices. - Cluster rows/columns if appropriate. --- ## Style Rules ### Colors **Wong colorblind-safe palette (default):** ```python WONG = ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7'] ``` **Sequential palettes (for heatmaps):** - Positive values: `Blues` or `Greens` - Diverging (centered at 0): `coolwarm` or `RdBu_r` - Agreement matrices: `YlOrRd` **Rules:** - Never use red-green only distinctions. - Use line style (solid, dashed, dotted) in addition to color for line plots. - Use marker shape in addition to color for scatter plots. ### Typography | Element | Font Size | Weight | |---------|-----------|--------| | Figure title (if any) | 10 pt | Bold | | Axis label | 9 pt | Regular | | Tick label | 8 pt | Regular | | Legend text | 8 pt | Regular | | Annotation | 8 pt | Regular | | Panel label (A, B, C) | 12 pt | Bold | - Font family: Arial or Helvetica (sans-serif). - Panel labels: uppercase bold letter, top-left of each panel. ### Layout - Minimize white space while maintaining readability. - Align multi-panel figures on a grid. - Consistent axis ranges across comparable panels. - No figure titles in the plot itself (title goes in the caption below). ### Statistical Annotations - Significance stars: * p<0.05, ** p<0.01, *** p<0.001 - Place above comparison brackets. - Report exact p-value in the figure legend or caption, not in the plot. - For AUC, correlation, or agreement: display in the legend with 95% CI. --- ## Journal Specifications Default dimensions (override from `figure_specs.md` if journal-specific): - **Single column**: 3.5 in (88 mm) width - **1.5 column**: 5.0 in (127 mm) width - **Double column**: 7.0 in (178 mm) width - **Full page**: 7.0 x 9.5 in (178 x 241 mm) - **DPI**: 300 minimum for halftone, 600 for line art - **File formats**: PDF (vector, preferred) + PNG (300 DPI) - **No chartjunk**: no 3D effects, no unnecessary gridlines, no decorative elements, no gradient fills --- ## Multi-Panel Figures For composite figures with multiple panels: ```python fig, axes = plt.subplots(nrows, ncols, figsize=(width, height)) # Label each panel for ax, label in zip(axes.flat, 'ABCDEFGH'): ax.text(-0.15, 1.05, label, transform=ax.transAxes, fontsize=12, fontweight='bold', va='top') ``` Common layouts: - 2-panel horizontal: `figsize=(7.0, 3.5)`, 1 row x 2 cols - 2-panel vertical: `figsize=(3.5, 7.0)`, 2 rows x 1 col - 2x2 grid: `figsize=(7.0, 7.0)`, 2 rows x 2 cols - 3-panel: `figsize=(7.0, 3.0)`, 1 row x 3 cols Use `plt.tight_layout()` or `fig.subplots_adjust()` for spacing. --- ## Caption Writing After generating each figure, draft a caption following these rules: 1. **First sentence**: Describe what the figure shows (type + key finding). 2. **Subsequent sentences**: Define abbreviations, explain symbols, state sample sizes. 3. **Statistical details**: Note the test used and significance threshold. 4. **Format**: "Figure {N}. {Caption text}" -- no bold, no title case. Example: > Figure 1. Receiver operating characteristic curves comparing the diagnostic performance of > the multi-agent pipeline (blue) and single-agent baseline (orange) for identifying incorrect > Anki flashcard content. The area under the curve was 0.92 (95% CI: 0.89-0.95) for the > multi-agent pipeline and 0.84 (95% CI: 0.80-0.88) for the single-agent baseline (DeLong > test, p = 0.003). The dashed diagonal line represents chance performance. --- ## Skill Interactions | When | Call | Purpose | |------|------|---------| | Need statistical values for plot | `/analyze-stats` | Get computed values (AUC, CI, p-values) | | Flow diagram for manuscript | `/write-paper` Phase 2 | Coordinate with Tables & Figures plan | | Caption review | `/write-paper` Phase 7 | Final polish pass | --- ## Error Handling - If data is insufficient for the requested figure type, explain what is needed and ask the user. - If a figure exceeds journal dimension limits, resize and report the adjustment. - If text overlaps in the figure, try `tight_layout()`, reduce font size, or adjust spacing. - Never fabricate data points. If sample data is needed for a template demo, explicitly label it as "example data." ## CLI Tools Available ImageMagick, Ghostscript, FFmpeg are installed and can be used for post-processing: ```bash # Figure DPI/format conversion for journal submission magick input.png -density 300 -units PixelsPerInch output.tiff magick input.png -resize 1200x -quality 95 output.jpg # CMYK conversion (some print journals require this) magick input.png -colorspace CMYK output.tiff ``` ### Portal-ready TIFF (SNAPP `.png`-not-accepted / 25 MB cap) A raw `magick ... output.tiff` keeps the alpha channel (transparent regions print **black** on many production pipelines) and stays uncompressed (a 600-dpi RGBA TIFF blows past a portal's 25 MB cap). `export_portal_tiff.py` does the flatten-and-compress a human otherwise does by hand and **verifies the result is pixel-identical** to that white-flatten before handing it over — use it when a portal accepts only `.tiff`/`.jpeg`/`.eps` (Springer Nature SNAPP) or caps figure size (JACC: Asia): ```bash python3 scripts/export_portal_tiff.py --in figure.png --out figure.tiff --max-mb 25 # LZW-compressed, RGBA→RGB white-flattened, pixel-identity-verified; exit 1 if still over the cap ``` ```bash # Multi-panel figure assembly (A/B/C/D panels) magick montage panelA.png panelB.png panelC.png panelD.png \ -tile 2x2 -geometry +10+10 -density 300 combined.png # Animated figure (GIF from frame sequence) ffmpeg -framerate 2 -i frame_%03d.png -vf "scale=800:-1" output.gif # Video from figure sequence (for supplementary materials) ffmpeg -framerate 1 -i slide_%03d.png -c:v libx264 -pix_fmt yuv420p supplementary_video.mp4 ``` ## AI Image Generation (Optional) AI illustration is a **supplementary option**, not a requirement. Visual abstracts and figures can be completed without any API key using study figures and free illustration libraries. If `GEMINI_API_KEY` is set, the `generate_image.py` script can generate illustrations: ```bash python ${CLAUDE_SKILL_DIR}/scripts/generate_image.py \ "Clean medical illustration of a CT-guided lung biopsy procedure, \ flat vector style, white background, no text" \ --output output.png --aspect 16:9 ``` Use for: procedural schematics, anatomical illustrations, pipeline diagrams. Always review AI output against the AI-Generated Figure Warning section above. If `GEMINI_API_KEY` is not set, guide the user to free illustration resources: see `${CLAUDE_SKILL_DIR}/references/medical_illustration_sources.md`. ## Language - Code and figure text: English - Communication with user: Match user's preferred language - Medical terms: English only ## Anti-Hallucination - **Never fabricate references.** All citations must be verified via `/search-lit` with confirmed DOI or PMID. Mark unverified references as `[UNVERIFIED - NEEDS MANUAL CHECK]`. - **Never invent clinical definitions, diagnostic criteria, or guideline recommendations.** If uncertain, flag with `[VERIFY]` and ask the user. ## Global-rule references Some passages in this skill cite a path of the form `~/.claude/rules/<name>.md`. Those are the maintainer's personal global rules, kept outside this repository. They are **not shipped with this skill** and will not exist on your machine; they appear only as provenance for where a convention came from. If one of them looks like it is standing in for an instruction you actually need, that is a bug — please open an issue, because the instruction belongs here. -
skill.yml 2.3 KB
schema_version: 2 name: make-figures version: 1.1.0 layer: B owner_domain: figure_generation maturity: official when_to_use: - User requests a figure, plot, diagram, or visual abstract for a medical research manuscript - Triggered by /write-paper Phase 5 (figure generation step) - Following /analyze-stats output that requires visualization - Generating PRISMA / CONSORT / STARD / STROBE flow diagrams - Producing journal-specific visual or graphical abstracts when_NOT_to_use: - Self-contained tabular results (use /analyze-stats markdown output) - Decorative slides for talks (use /present-paper) - Logos, branding, or non-research illustrations (out of scope) - AI-generated images for journals that prohibit them (JACC family, NEJM) — see Journal AI-Image Policies in SKILL.md inputs: - analysis/_analysis_outputs.md - analysis/tables/*.csv outputs: - analysis/figures/_figure_manifest.md - analysis/figures/*.png - analysis/figures/*.pdf deterministic_scripts: - scripts/generate_flow_diagram.R - scripts/critic_figure.py - scripts/generate_visual_abstract.py - scripts/validate_pptx_mac_compat.py side_effects: - writes_figure_artifacts - reads_medical_illustration_assets_from_servier_or_bioart downstream_consumers: - write-paper - sync-submission - present-paper forbidden_actions: - fabricate_figure_numbers - create_flow_diagrams_without_source_counts - generate_AI_images_for_prohibited_targets # v2.1 quality card purpose: "Generate publication-ready figures and visual/graphical abstracts (ROC, forest, CONSORT/STARD/PRISMA flow, KM, Bland-Altman, etc.)." safety_boundaries: - "Figure numbers are not fabricated; flow diagrams are built from real source counts." - "Honors journal AI-image policies; no AI images for prohibited targets." known_limitations: - "Figure correctness depends on correct input data/counts supplied by upstream skills." - "PPTX visual abstracts need the Mac-compatibility check before sharing." validation_commands: - "Rscript scripts/generate_flow_diagram.R" - "python3 scripts/validate_pptx_mac_compat.py <file>" - "python3 tests/test_pptx_mac_compat.py" - "bash scripts/render_core_figures_challenge/verify.sh" - "bash scripts/export_portal_tiff_challenge/verify.sh" evidence_surface: demo
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.