scientific-figure-assembly

📁 htlin222/dotfiles 📅 4 days ago
1
总安装量
1
周安装量
#54077
全站排名
安装命令
npx skills add https://github.com/htlin222/dotfiles --skill scientific-figure-assembly

Agent 安装分布

mcpjam 1
claude-code 1
replit 1
junie 1
windsurf 1
zencoder 1

Skill 文档

Scientific Figure Assembly (R-based)

Create publication-ready multi-panel figures using R packages (patchwork, cowplot) with professional panel labels (A, B, C, D) at 300 DPI resolution.

⚠️ IMPORTANT: This workflow uses R for figure assembly. For meta-analysis projects, all figures should be generated and assembled in R.

When to Use

  • Combining multiple plots into a single multi-panel figure for publication
  • Adding panel labels (A, B, C) to existing figures
  • Ensuring figures meet journal requirements (300 DPI minimum)
  • Creating consistent figure layouts for manuscripts
  • Preparing figures for Nature, Science, Cell, JAMA, Lancet submissions

Quick Start

Tell me:

  1. Plot objects: R plot objects (ggplot, forest plots, etc.) OR paths to PNG/JPG files
  2. Layout: Vertical (stacked), horizontal (side-by-side), or grid (2×2, 2×3, etc.)
  3. Output name: What to call the final figure
  4. Labels: Which panel labels to use (default: A, B, C, D…)

I’ll create an R script using patchwork or cowplot to assemble the figure with proper spacing and labels.

R Package Approach (Recommended)

Method 1: patchwork (For ggplot2 objects)

The simplest and most powerful method for combining ggplot2 objects:

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)

Method 2: cowplot (For any R plots)

More flexible, works with base R plots and 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)

Legacy Python Script Template (Not Recommended)

⚠️ For meta-analysis projects, use R methods above instead.

If you absolutely need Python for existing PNG files:

#!/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)

Common Layouts

Vertical (Most Common)

Stack plots on top of each other – good for showing progression or related outcomes.

Example: Three forest plots (pCR, EFS, OS) stacked vertically

  • Panel A: pCR forest plot
  • Panel B: EFS forest plot
  • Panel C: OS forest plot

Horizontal

Place plots side-by-side – good for comparisons.

Example: Two funnel plots showing publication bias

  • Panel A: pCR funnel plot
  • Panel B: EFS funnel plot

Grid (2×2, 2×3, etc.)

Arrange in rows and columns – good for systematic comparisons.

Example: 2×2 grid of subgroup analyses

  • Panel A: Age subgroup
  • Panel B: Sex subgroup
  • Panel C: Stage subgroup
  • Panel D: Histology subgroup

R Workflow (Recommended)

Complete Example: Meta-Analysis Forest Plots

#!/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")

Using cowplot for More Control

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")

Grid Layout (2×2 or 2×3)

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 Workflow (Legacy – For PNG Files Only)

⚠️ Only use if you have existing PNG files and cannot regenerate in R.

Step 1: Verify Input Files

# Check that all files exist and are PNG/JPG
ls -lh path/to/plots/*.png

Step 2: Create Assembly Script

Use the Python template provided in this skill.

Step 3: Run Assembly

# 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

Step 4: Verify Output

# Check dimensions and file size
ls -lh Figure1_Efficacy.png

# Verify DPI (should show 300x300)
file Figure1_Efficacy.png

Customization Options

Font Size Adjustment

For different image sizes:

  • 3000px wide images: font_size=80 (default)
  • 1500px wide images: font_size=40
  • 6000px wide images: font_size=160

Label Position

  • position='top-left' (default)
  • position='top-right'
  • position='bottom-left'
  • position='bottom-right'

Spacing Between Panels

  • Default: spacing=40 pixels
  • Tight spacing: spacing=20
  • Loose spacing: spacing=80

Label Style

  • White background with black border (default, best visibility)
  • Transparent background: bg_color=None, border=False
  • Custom colors: bg_color='#f0f0f0', text_color='#333333'

Journal Requirements

Nature, Science, Cell

  • Resolution: 300-600 DPI
  • Format: TIFF or high-quality PDF preferred, PNG acceptable
  • Width: 89mm (single column) or 183mm (double column) at final size
  • Font: Arial, Helvetica, or similar sans-serif
  • Labels: Bold, 8-10pt at final size

Lancet, JAMA, NEJM

  • Resolution: 300 DPI minimum
  • Format: TIFF, EPS, or PNG
  • Width: Fit within column width (typically 3-4 inches)
  • Labels: Clear, high contrast
  • Grayscale: Must be readable in B&W

Quality Checklist

Before submitting:

  • All figures at 300 DPI minimum
  • Panel labels (A, B, C) visible and correctly ordered
  • Labels don’t obscure important data
  • All panels aligned properly
  • Spacing consistent between panels
  • File size reasonable (<10 MB for PNG)
  • Figures readable when printed at final journal size
  • Color schemes work in grayscale (if required)

Common Issues & Solutions

Problem: Labels too small Solution: Increase font_size parameter (try doubling it)

Problem: Labels obscure data Solution: Change position to different corner or adjust offset

Problem: DPI too low Solution: Regenerate input plots at higher resolution first, then reassemble

Problem: Uneven spacing Solution: Crop input images to remove excess white space before assembly

Problem: File too large Solution: Use PNG compression or convert to JPEG (may lose quality)

Example Use Cases

Meta-Analysis Figures (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)

Legacy Python Examples (Not Recommended)

# 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

Dependencies

R Packages (Recommended)

# Install from CRAN
install.packages(c("patchwork", "cowplot", "ggplot2"))

# For meta-analysis plots
install.packages(c("meta", "metafor"))

Python (Legacy – Only for PNG Assembly)

# Install using uv (if needed for legacy workflows)
uv add Pillow

# Or using pip
pip install Pillow

Output Example

R Output

✅ Created figure1_efficacy.png
   Dimensions: 3000×4200 px at 300 DPI
   Size: 2.3 MB

The output file will have:

  • Professional panel labels (A, B, C) automatically added by patchwork/cowplot
  • Consistent spacing between panels
  • 300 DPI resolution suitable for publication
  • Aligned axes for easy comparison
  • Publication-ready theme

Python Output (Legacy)

✅ Created Figure1_Efficacy.png
   Dimensions: 3000×6080 px at 300 DPI

The output file will have:

  • Professional panel labels (A, B, C) in top-left corners
  • Consistent spacing between panels
  • 300 DPI resolution suitable for publication
  • White background with black border around labels for maximum visibility

Related Skills

  • /meta-manuscript-assembly – Complete meta-analysis manuscript preparation
  • /plot-publication – Create individual publication-ready plots
  • /figure-legends – Generate comprehensive figure legends

Pro Tips

R Workflow Tips

  1. Work in R throughout: Generate plots AND assemble in R for best results
  2. Use patchwork for ggplot2: Simplest syntax (p1 / p2 for vertical)
  3. Use cowplot for mixed plots: Works with base R and ggplot2
  4. Set theme globally: Use theme_set(theme_minimal()) for consistency
  5. Export once at end: Create all plots, combine, then export (faster)
  6. Check alignment: Use align = "v" and axis = "l" in cowplot
  7. Consistent sizes: Set base_size in theme for readable text

General Tips

  1. Generate high-quality inputs first: Assembly won’t improve low-quality source plots
  2. Label systematically: A-B-C top-to-bottom or left-to-right
  3. Check in print preview: Ensure labels readable at final print size
  4. Keep R scripts: Save R code for reproducibility
  5. Version control figures: Commit both R scripts and final PNG files
  6. Test on different screens: Check readability on laptop and printed page

R Package Resources

When you need help with R packages: