关于 scientific-figure-assembly
面向需要为论文整理多面板图表的研究者,指导使用 R 的 patchwork 或 cowplot 组合图表并添加面板标签。重点包括一致的布局、对齐和 300 DPI 输出;现有 PNG/JPG 也可使用旧版 Python 流程。
主要功能
- 01组合多个图表为多面板布局
- 02添加 A、B、C 等面板标签
- 03支持纵向、横向和网格布局
- 04使用 patchwork 或 cowplot 进行 R 组图
- 05按至少 300 DPI 准备输出
适用场景
- 01组合荟萃分析中的森林图
- 02并排呈现多个结果图以便比较
- 03为期刊投稿整理并标注图表
- 04将现有 PNG/JPG 组装为多面板图
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ htlin222/ dotfiles --skill scientific-figure-assembly安装后在 Claude Code 中输入/scientific-figure-assembly确认已加载。
附带文件scripts/assemble_figures.py
详细介绍
科学图表组装(基于 R)
使用 R 包(patchwork、cowplot)创建可供发表的多面板图,并添加专业的面板标签(A、B、C、D),分辨率为 300 DPI。
⚠️ 重要:此工作流使用 R 组装图表。对于荟萃分析项目,所有图表都应使用 R 生成和组装。
适用场景
- 将多个图表合并为一张可供发表的多面板图
- 为现有图表添加面板标签(A、B、C)
- 确保图表符合期刊要求(最低 300 DPI)
- 为论文创建统一的图表布局
- 为 Nature、Science、Cell、JAMA、Lancet 投稿准备图表
快速入门
请告诉我:
- 图表对象:R 图表对象(ggplot、森林图等)或 PNG/JPG 文件路径
- 布局:垂直(堆叠)、水平(并排)或网格(2x2、2x3 等)
- 输出名称:最终图表的名称
- 标签:要使用的面板标签(默认:A、B、C、D……)
我将使用 patchwork 或 cowplot 编写 R 脚本,以适当的间距和标签组装图表。
R 包方法(推荐)
方法 1:patchwork(适用于 ggplot2 对象)
合并 ggplot2 对象最简单、功能最强大的方法:
library(ggplot2)
library(patchwork)
# Create or load individual plots
p1 <- ggplot(data1, aes(x, y)) + geom_point() + ggtitle("A. First Panel")
p2 <- ggplot(data2, aes(x, y)) + geom_line() + ggtitle("B. Second Panel")
p3 <- ggplot(data3, aes(x, y)) + geom_bar(stat="identity") + ggtitle("C. Third Panel")
# Combine vertically
combined <- p1 / p2 / p3
# Or combine horizontally
combined <- p1 | p2 | p3
# Or grid layout (2 columns)
combined <- (p1 | p2) / p3
# Export at 300 DPI
ggsave("figures/figure1_combined.png",
plot = combined,
width = 10, height = 12, dpi = 300)
方法 2:cowplot(适用于任何 R 图表)
更灵活,可用于基础 R 图表和 ggplot2:
library(ggplot2)
library(cowplot)
# Create individual plots
p1 <- ggplot(data1, aes(x, y)) + geom_point()
p2 <- ggplot(data2, aes(x, y)) + geom_line()
p3 <- ggplot(data3, aes(x, y)) + geom_bar(stat="identity")
# Combine with automatic panel labels
combined <- plot_grid(
p1, p2, p3,
labels = c("A", "B", "C"),
label_size = 18,
ncol = 1, # Vertical stack
rel_heights = c(1, 1, 1) # Equal heights
)
# Export
ggsave("figures/figure1_combined.png",
plot = combined,
width = 10, height = 12, dpi = 300)
旧版 Python 脚本模板(不推荐)
⚠️ 对于荟萃分析项目,请改用上述 R 方法。
如果确实需要使用 Python 处理现有 PNG 文件:
#!/usr/bin/env python3
"""Legacy: Assemble multi-panel scientific figure from PNG files."""
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
def add_panel_label(img, label, position='top-left',
font_size=80, offset=(40, 40),
bg_color='white', text_color='black',
border=True):
"""
Add panel label (A, B, C) to image.
Args:
img: PIL Image object
label: Label text (e.g., 'A', 'B', 'C')
position: 'top-left', 'top-right', 'bottom-left', 'bottom-right'
font_size: Font size in pixels (80 works well for 3000px wide images)
offset: (x, y) offset from corner in pixels
bg_color: Background color for label box
text_color: Label text color
border: Whether to draw border around label box
"""
draw = ImageDraw.Draw(img)
# Try system fonts (macOS, then Linux)
try:
font = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", font_size)
except:
try:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", font_size
)
except:
font = ImageFont.load_default()
print(f"Warning: Using default font for label {label}")
# Calculate label position
x, y = offset
if 'right' in position:
bbox = draw.textbbox((0, 0), label, font=font)
text_width = bbox[2] - bbox[0]
x = img.width - text_width - offset[0]
if 'bottom' in position:
bbox = draw.textbbox((0, 0), label, font=font)
text_height = bbox[3] - bbox[1]
y = img.height - text_height - offset[1]
# Draw background box
bbox = draw.textbbox((x, y), label, font=font)
padding = 10
draw.rectangle(
[bbox[0] - padding, bbox[1] - padding,
bbox[2] + padding, bbox[3] + padding],
fill=bg_color,
outline='black' if border else None,
width=2 if border else 0
)
# Draw text
draw.text((x, y), label, fill=text_color, font=font)
return img
def assemble_vertical(input_files, output_file, labels=None,
spacing=40, dpi=300):
"""
Stack images vertically with panel labels.
Args:
input_files: List of paths to input images
output_file: Path for output image
labels: List of labels (default: A, B, C, ...)
spacing: Vertical spacing between panels in pixels
dpi: Output resolution
"""
if labels is None:
labels = [chr(65 + i) for i in range(len(input_files))] # A, B, C, ...
# Load all images
images = [Image.open(f) for f in input_files]
# Add labels
labeled = [add_panel_label(img, label)
for img, label in zip(images, labels)]
# Calculate dimensions
max_width = max(img.width for img in labeled)
total_height = sum(img.height for img in labeled) + spacing * (len(labeled) - 1)
# Create combined image
combined = Image.new('RGB', (max_width, total_height), 'white')
# Paste images
y_offset = 0
for img in labeled:
combined.paste(img, (0, y_offset))
y_offset += img.height + spacing
# Save with specified DPI
combined.save(output_file, dpi=(dpi, dpi))
print(f"✅ Created {output_file}")
print(f" Dimensions: {combined.width}×{combined.height} px at {dpi} DPI")
return output_file
def assemble_horizontal(input_files, output_file, labels=None,
spacing=40, dpi=300):
"""Stack images horizontally with panel labels."""
if labels is None:
labels = [chr(65 + i) for i in range(len(input_files))]
images = [Image.open(f) for f in input_files]
labeled = [add_panel_label(img, label)
for img, label in zip(images, labels)]
max_height = max(img.height for img in labeled)
total_width = sum(img.width for img in labeled) + spacing * (len(labeled) - 1)
combined = Image.new('RGB', (total_width, max_height), 'white')
x_offset = 0
for img in labeled:
combined.paste(img, (x_offset, 0))
x_offset += img.width + spacing
combined.save(output_file, dpi=(dpi, dpi))
print(f"✅ Created {output_file}")
print(f" Dimensions: {combined.width}×{combined.height} px at {dpi} DPI")
return output_file
def assemble_grid(input_files, output_file, rows, cols,
labels=None, spacing=40, dpi=300):
"""
Arrange images in a grid with panel labels.
Args:
rows: Number of rows
cols: Number of columns
Other args same as assemble_vertical
"""
if labels is None:
labels = [chr(65 + i) for i in range(len(input_files))]
images = [Image.open(f) for f in input_files]
labeled = [add_panel_label(img, label)
for img, label in zip(images, labels)]
# Calculate cell dimensions (use max from each row/col)
cell_width = max(img.width for img in labeled)
cell_height = max(img.height for img in labeled)
# Total dimensions
total_width = cell_width * cols + spacing * (cols - 1)
total_height = cell_height * rows + spacing * (rows - 1)
combined = Image.new('RGB', (total_width, total_height), 'white')
# Place images
for idx, img in enumerate(labeled):
if idx >= rows * cols:
break
row = idx // cols
col = idx % cols
x = col * (cell_width + spacing)
y = row * (cell_height + spacing)
combined.paste(img, (x, y))
combined.save(output_file, dpi=(dpi, dpi))
print(f"✅ Created {output_file}")
print(f" Dimensions: {combined.width}×{combined.height} px at {dpi} DPI")
return output_file
if __name__ == '__main__':
import sys
# Example usage
if len(sys.argv) < 3:
print("Usage: python assemble_figures.py <output> <layout> <input1> <input2> ...")
print(" layout: vertical, horizontal, or grid:RxC (e.g., grid:2x2)")
sys.exit(1)
output = sys.argv[1]
layout = sys.argv[2]
inputs = sys.argv[3:]
if layout == 'vertical':
assemble_vertical(inputs, output)
elif layout == 'horizontal':
assemble_horizontal(inputs, output)
elif layout.startswith('grid:'):
rows, cols = map(int, layout.split(':')[1].split('x'))
assemble_grid(inputs, output, rows, cols)
else:
print(f"Unknown layout: {layout}")
sys.exit(1)
常见布局
垂直(最常见)
将图表上下堆叠,适合展示进展或相关结果。
示例:垂直堆叠三张森林图(pCR、EFS、OS)
- 面板 A:pCR 森林图
- 面板 B:EFS 森林图
- 面板 C:OS 森林图
水平
将图表并排放置,适合进行比较。
示例:两张展示发表偏倚的漏斗图
- 面板 A:pCR 漏斗图
- 面板 B:EFS 漏斗图
网格(2x2、2x3 等)
按行和列排列,适合进行系统比较。
示例:亚组分析的 2x2 网格
- 面板 A:年龄亚组
- 面板 B:性别亚组
- 面板 C:分期亚组
- 面板 D:组织学亚组
R 工作流(推荐)
完整示例:荟萃分析森林图
#!/usr/bin/env Rscript
# assemble_forest_plots.R
# Combine multiple forest plots into a single figure
library(meta)
library(metafor)
library(patchwork)
# Set working directory
setwd("/Users/htlin/meta-pipe/06_analysis")
# Load extraction data
data <- read.csv("../05_extraction/extraction.csv")
# --- Create individual forest plots ---
# Plot 1: Pathologic complete response
res_pcr <- metabin(
event.e = events_pcr_ici,
n.e = total_ici,
event.c = events_pcr_control,
n.c = total_control,
data = data,
studlab = study_id,
sm = "RR",
method = "MH"
)
# Save as ggplot-compatible object
p1 <- forest(res_pcr, layout = "RevMan5") +
ggtitle("A. Pathologic Complete Response")
# Plot 2: Event-free survival
res_efs <- metagen(
TE = log_hr_efs,
seTE = se_log_hr_efs,
data = data,
studlab = study_id,
sm = "HR"
)
p2 <- forest(res_efs) +
ggtitle("B. Event-Free Survival")
# Plot 3: Overall survival
res_os <- metagen(
TE = log_hr_os,
seTE = se_log_hr_os,
data = data,
studlab = study_id,
sm = "HR"
)
p3 <- forest(res_os) +
ggtitle("C. Overall Survival")
# --- Combine with patchwork ---
combined <- p1 / p2 / p3 +
plot_annotation(
title = "Figure 1. Efficacy Outcomes with ICI vs Control",
theme = theme(plot.title = element_text(size = 16, face = "bold"))
)
# Export at 300 DPI
ggsave("../07_manuscript/figures/figure1_efficacy.png",
plot = combined,
width = 10,
height = 14,
dpi = 300,
bg = "white")
cat("✅ Created figure1_efficacy.png\n")
cat(" Dimensions: 3000×4200 px at 300 DPI\n")
使用 cowplot 获得更多控制
library(cowplot)
# Combine with explicit panel labels and alignment
combined <- plot_grid(
p1, p2, p3,
labels = c("A", "B", "C"),
label_size = 18,
label_fontface = "bold",
ncol = 1,
align = "v", # Vertical alignment
axis = "l", # Align left axis
rel_heights = c(1, 1, 1)
)
# Add overall title
title <- ggdraw() +
draw_label(
"Figure 1. Efficacy Outcomes with ICI vs Control",
fontface = "bold",
size = 16,
x = 0.5,
hjust = 0.5
)
# Combine title and plots
final <- plot_grid(
title,
combined,
ncol = 1,
rel_heights = c(0.1, 1)
)
# Export
ggsave("../07_manuscript/figures/figure1_efficacy.png",
plot = final,
width = 10, height = 14, dpi = 300, bg = "white")
网格布局(2x2 或 2x3)
library(patchwork)
# 2x2 grid
combined <- (p1 | p2) / (p3 | p4) +
plot_annotation(tag_levels = "A")
# 2x3 grid
combined <- (p1 | p2 | p3) / (p4 | p5 | p6) +
plot_annotation(tag_levels = "A")
ggsave("figure_grid.png", width = 14, height = 10, dpi = 300)
Python 工作流(旧版——仅适用于 PNG 文件)
⚠️ 仅当你有现成的 PNG 文件且无法在 R 中重新生成时才使用。
步骤 1:验证输入文件
# Check that all files exist and are PNG/JPG
ls -lh path/to/plots/*.png
步骤 2:创建组装脚本
使用此技能提供的 Python 模板。
步骤 3:运行组装
# Using uv (recommended for dependency management)
uv run python assemble_figures.py Figure1_Efficacy.png vertical \
forest_plot_pCR.png \
forest_plot_EFS.png \
forest_plot_OS.png
# Or with system Python (requires PIL/Pillow)
python assemble_figures.py Figure1.png grid:2x2 \
plot1.png plot2.png plot3.png plot4.png
步骤 4:验证输出
# Check dimensions and file size
ls -lh Figure1_Efficacy.png
# Verify DPI (should show 300x300)
file Figure1_Efficacy.png
自定义选项
调整字体大小
适用于不同尺寸的图像:
- 宽度为 3000px 的图像:
font_size=80(默认) - 宽度为 1500px 的图像:
font_size=40 - 宽度为 6000px 的图像:
font_size=160
标签位置
position='top-left'(默认)position='top-right'position='bottom-left'position='bottom-right'
面板间距
- 默认:
spacing=40像素 - 紧凑间距:
spacing=20 - 宽松间距:
spacing=80
标签样式
- 白色背景,黑色边框(默认,可见度最佳)
- 透明背景:
bg_color=None, border=False - 自定义颜色:
bg_color='#f0f0f0', text_color='#333333'
期刊要求
Nature、Science、Cell
- 分辨率:300–600 DPI
- 格式:优先使用 TIFF 或高质量 PDF,也可使用 PNG
- 宽度:按最终尺寸,单栏为 89mm,双栏为 183mm
- 字体:Arial、Helvetica 或类似的无衬线字体
- 标签:按最终尺寸使用粗体 8–10pt
Lancet、JAMA、NEJM
- 分辨率:最低 300 DPI
- 格式:TIFF、EPS 或 PNG
- 宽度:适合栏宽(通常为 3–4 英寸)
- 标签:清晰、对比度高
- 灰度:必须确保黑白打印时仍清晰可读
质量检查清单
提交前:
- 所有图的分辨率至少为 300 DPI
- 面板标签(A、B、C)清晰可见且顺序正确
- 标签没有遮挡重要数据
- 所有面板均已正确对齐
- 面板之间的间距一致
- 文件大小合理(PNG 小于 10 MB)
- 图在期刊最终印刷尺寸下仍清晰可读
- 配色方案在灰度模式下仍适用(如有要求)
常见问题与解决方案
问题:标签太小
解决方案:增大 font_size 参数(可尝试将其加倍)
问题:标签遮挡数据
解决方案:将 position 更改为其他角落,或调整 offset
问题:DPI 太低 解决方案:先以更高分辨率重新生成输入图,再重新组装
问题:间距不均匀 解决方案:组装前裁剪输入图像,去除多余的空白区域
问题:文件太大 解决方案:使用 PNG 压缩,或转换为 JPEG(可能会损失质量)
示例用例
Meta-Analysis 图(R)
# Figure 1: Efficacy outcomes (3 vertical panels)
library(patchwork)
combined <- p_pcr / p_efs / p_os +
plot_annotation(
title = "Figure 1. Efficacy Outcomes",
tag_levels = "A"
)
ggsave("07_manuscript/figures/figure1_efficacy.png",
width = 10, height = 14, dpi = 300)
# Figure 2: Safety + Bias (2 vertical panels)
combined <- p_safety / p_funnel +
plot_annotation(tag_levels = "A")
ggsave("07_manuscript/figures/figure2_safety.png",
width = 10, height = 10, dpi = 300)
# Figure 3: Subgroup analysis (2x2 grid)
combined <- (p_age | p_sex) / (p_stage | p_histology) +
plot_annotation(
title = "Figure 3. Subgroup Analyses",
tag_levels = "A"
)
ggsave("07_manuscript/figures/figure3_subgroups.png",
width = 14, height = 12, dpi = 300)
旧版 Python 示例(不推荐)
# Figure 1: Efficacy outcomes (3 vertical panels)
uv run python assemble.py Figure1_Efficacy.png vertical \
forest_plot_pCR.png \
forest_plot_EFS.png \
forest_plot_OS.png
# Figure 2: Safety + Bias (2 vertical panels)
uv run python assemble.py Figure2_Safety.png vertical \
forest_plot_SAE.png \
funnel_plot_pCR.png
依赖项
R 包(推荐)
# Install from CRAN
install.packages(c("patchwork", "cowplot", "ggplot2"))
# For meta-analysis plots
install.packages(c("meta", "metafor"))
Python(旧版——仅用于 PNG 组装)
# Install using uv (if needed for legacy workflows)
uv add Pillow
# Or using pip
pip install Pillow
输出示例
R 输出
✅ Created figure1_efficacy.png
Dimensions: 3000×4200 px at 300 DPI
Size: 2.3 MB
输出文件将具备以下特点:
- patchwork/cowplot 自动添加的专业面板标签(A、B、C)
- 面板之间的间距一致
- 适合发表的 300 DPI 分辨率
- 坐标轴对齐,便于比较
- 符合发表要求的主题
Python 输出(旧版)
✅ Created Figure1_Efficacy.png
Dimensions: 3000×6080 px at 300 DPI
输出文件将具备以下特点:
- 左上角的专业面板标签(A、B、C)
- 面板之间的间距一致
- 适合发表的 300 DPI 分辨率
- 白色背景,标签周围有黑色边框,确保清晰可见
相关技能
/meta-manuscript-assembly- 完成 Meta-Analysis 手稿准备/plot-publication- 创建适合发表的单张图/figure-legends- 生成全面的图例说明
实用技巧
R 工作流技巧
- 全程使用 R:在 R 中生成并组装图,以获得最佳效果
- 使用 patchwork 处理 ggplot2 图:语法最简单(
p1 / p2表示垂直排列) - 使用 cowplot 处理混合图:兼容 base R 和 ggplot2
- 全局设置主题:使用
theme_set(theme_minimal())保持一致 - 最后统一导出:先创建所有图并组合,再导出(速度更快)
- 检查对齐情况:在 cowplot 中使用
align = "v"和axis = "l" - 统一尺寸:在主题中设置
base_size,确保文本清晰易读
通用技巧
- 先生成高质量的输入图:组装无法改善低质量的源图
- 系统地添加标签:按从上到下或从左到右的顺序标记 A、B、C
- 在打印预览中检查:确保标签在最终印刷尺寸下清晰可读
- 保留 R 脚本:保存 R 代码以便复现
- 对图进行版本控制:同时提交 R 脚本和最终 PNG 文件
- 在不同屏幕上测试:检查图在笔记本电脑屏幕和打印页面上的可读性
R 包资源
需要 R 包方面的帮助时:
- CRAN:https://cran.r-project.org/(patchwork、cowplot 文档)
- Tidyverse:https://www.tidyverse.org/(ggplot2 参考文档)
- R-universe:https://r-universe.dev/search/(搜索所有 R 包)
- patchwork 指南:https://patchwork.data-imaginist.com/
- cowplot 指南:https://wilkelab.org/cowplot/