scientific-figure-assembly

安装量: 35
排名: #19744

安装

npx skills add https://github.com/htlin222/dotfiles --skill scientific-figure-assembly
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:
Plot objects
R plot objects (ggplot, forest plots, etc.) OR paths to PNG/JPG files
Layout
Vertical (stacked), horizontal (side-by-side), or grid (2x2, 2x3, etc.)
Output name
What to call the final figure
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 ..."
)
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 (2x2, 2x3, etc.)
Arrange in rows and columns - good for systematic comparisons.
Example
2x2 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 (2x2 or 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 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

返回排行榜