Killer-Skills Review
Decision support comes first. Repository text comes second.
This page remains useful for operators, but Killer-Skills treats it as reference material instead of a primary organic landing page.
Идеально подходит для агентов научного анализа, которым необходимы расширенные возможности графиков для создания публикаций высокого качества. Create publication-quality figures and graphs for scientific analysis. Use when creating bar charts, ROC curves, confusion matrices, scatter plots, heatmaps, 3D surface plots, or any data visualization. Supports PDF and PNG output with consistent styling.
Зачем использовать этот навык
Позволяет агентам создавать публикации высокого качества для научных статей и отчетов с помощью Python, с автоматической синхронизацией PowerPoint через файлы .pptx, и создает PNG-файлы для бесшовной интеграции.
Подходит лучше всего
Идеально подходит для агентов научного анализа, которым необходимы расширенные возможности графиков для создания публикаций высокого качества.
↓ Реализуемые кейсы использования for figure-graphing
! Безопасность и ограничения
- Требуется среда Python
- Ограничен созданием графиков только в формате PNG
- Зависит от скрипта `generate_paper_figures.py` для автоматизации
Why this page is reference-only
- - Current locale does not satisfy the locale-governance contract.
Source Boundary
The section below is imported from the upstream repository and should be treated as secondary evidence. Use the Killer-Skills review above as the primary layer for fit, risk, and installation decisions.
Decide The Next Action Before You Keep Reading Repository Material
Killer-Skills should not stop at opening repository instructions. It should help you decide whether to install this skill, when to cross-check against trusted collections, and when to move into workflow rollout.
Start With Installation And Validation
If this skill is worth continuing with, the next step is to confirm the install command, CLI write path, and environment validation.
Cross-Check Against Trusted Picks
If you are still comparing multiple skills or vendors, go back to the trusted collection before amplifying repository noise.
Move To Workflow Collections For Team Rollout
When the goal shifts from a single skill to team handoff, approvals, and repeatable execution, move into workflow collections.
Browser Sandbox Environment
⚡️ Ready to unleash?
Experience this Agent in a zero-setup browser environment powered by WebContainers. No installation required.
FAQ & Installation Steps
These questions and steps mirror the structured data on this page for better search understanding.
? Frequently Asked Questions
What is figure-graphing?
Идеально подходит для агентов научного анализа, которым необходимы расширенные возможности графиков для создания публикаций высокого качества. Create publication-quality figures and graphs for scientific analysis. Use when creating bar charts, ROC curves, confusion matrices, scatter plots, heatmaps, 3D surface plots, or any data visualization. Supports PDF and PNG output with consistent styling.
How do I install figure-graphing?
Run the command: npx killer-skills add NoahB10/Cardiac_RODEO/figure-graphing. It works with Cursor, Windsurf, VS Code, Claude Code, and 19+ other IDEs.
What are the use cases for figure-graphing?
Key use cases include: Автоматизировать создание графиков для научных статей, Создавать публикации высокого качества с автоматической синхронизацией PowerPoint, Создавать PNG-файлы для отчетов и презентаций.
Which IDEs are compatible with figure-graphing?
This skill is compatible with Cursor, Windsurf, VS Code, Trae, Claude Code, OpenClaw, Aider, Codex, OpenCode, Goose, Cline, Roo Code, Kiro, Augment Code, Continue, GitHub Copilot, Sourcegraph Cody, and Amazon Q Developer. Use the Killer-Skills CLI for universal one-command installation.
Are there any limitations for figure-graphing?
Требуется среда Python. Ограничен созданием графиков только в формате PNG. Зависит от скрипта `generate_paper_figures.py` для автоматизации.
↓ How To Install
-
1. Open your terminal
Open the terminal or command line in your project directory.
-
2. Run the install command
Run: npx killer-skills add NoahB10/Cardiac_RODEO/figure-graphing. The CLI will automatically detect your IDE or AI agent and configure the skill.
-
3. Start using the skill
The skill is now active. Your AI agent can use figure-graphing immediately in the current project.
! Reference-Only Mode
This page remains useful for installation and reference, but Killer-Skills no longer treats it as a primary indexable landing page. Read the review above before relying on the upstream repository instructions.
Upstream Repository Material
The section below is imported from the upstream repository and should be treated as secondary evidence. Use the Killer-Skills review above as the primary layer for fit, risk, and installation decisions.
figure-graphing
Install figure-graphing, an AI agent skill for AI agent workflows and automation. Review the use cases, limitations, and setup path before rollout.
Figure Graphing Skill
Create publication-quality figures for scientific papers and reports.
Automatic PowerPoint Sync
IMPORTANT: When generating figures using generate_paper_figures.py, the PowerPoint is automatically updated:
bash1# Generate figure and auto-update PowerPoint 2python generate_paper_figures.py --figure 3 3 4# Skip PowerPoint update (figures only) 5python generate_paper_figures.py --figure 3 --no-pptx
The script automatically:
- Generates figure PNGs to
Output/PowerPoint_Figures/Fig_X/ - Unpacks
Output/PowerPoint_Figures/Cardiac_RODEO_Tracked.pptx - Copies updated images to the correct media slots
- Repacks the PowerPoint
Figure-to-Image Mapping:
| Figure | PowerPoint Images | Slide |
|---|---|---|
| Fig_3 (a-e) | image4-8 | 3 |
| Fig_6 (a-h) | image11-18 | 6 |
| Fig_7 (a-h) | image19-26 | 7 |
| Fig_8 (a-f) | image27-32 | 8 |
Quick Reference
Standard Figure Types
- Bar Charts - Accuracy, AUC, metric comparisons (always with error bars)
- Grouped Bar Charts - Multiple metrics per model (always with error bars)
- ROC Curves - MUST have shaded confidence bands (bootstrap ±1 std, use
fill_between) - Confusion Matrices - With counts and percentages (
Bluescmap) - Scatter Plots - Per-drug/per-sample predictions
- Heatmaps - Correlation (
RdBu_r), performance (RdYlGn), SHAP (coolwarm) - Threshold Analysis - Horizontal scatter, drugs on Y-axis, green/red colors
- 3D Surface Plots - PK-PD response surfaces
- SHAP Aligned Pairs - Positive/negative SHAP paired by magnitude, blue/grey colors
- Accuracy vs AUC Scatter - X=Accuracy, Y=AUC, comparing equations across ML models
Color Palettes
Primary Color Palette (MANDATORY)
Use these colors consistently across ALL figures:
python1PRIMARY_COLORS = { 2 'beige': '#E3D5B2', # Warm neutral - backgrounds, secondary elements 3 'blue': '#6C92ED', # Primary accent - main data series 4 'pink': '#ECA0C0', # Secondary accent - comparison data 5 'orange': '#F8B274', # Tertiary accent - highlights 6} 7 8# Extended palette (same theme) 9EXTENDED_COLORS = { 10 'dark_blue': '#4A6FBF', # Darker blue for emphasis 11 'light_pink': '#F5C6D6', # Lighter pink for fills 12 'coral': '#E89B7A', # Warm coral 13 'sage': '#A8C4A2', # Muted green 14 'lavender': '#B8A9D9', # Soft purple 15 'cream': '#F5EFE0', # Light background 16}
Model Comparison Colors
python1colors = { 2 'CNN (DIQT Transfer)': '#ECA0C0', # Pink 3 'CNN (5-fold on 25)': '#6C92ED', # Blue 4 'Organoid (5-fold)': '#F8B274', # Orange 5 'ADMET-AI': '#6C92ED', # Blue 6 'SwissADME': '#ECA0C0', # Pink 7}
Metric Colors
python1metric_colors = { 2 'Accuracy': '#6C92ED', # Blue 3 'F1 Score': '#F8B274', # Orange 4 'MCC': '#ECA0C0', # Pink 5 'AUC': '#4A6FBF', # Dark Blue 6 'Sensitivity': '#E89B7A', # Coral 7 'Specificity': '#A8C4A2', # Sage 8}
Classification Colors
python1class_colors = { 2 'Positive': '#ECA0C0', # Pink 3 'Negative': '#6C92ED', # Blue 4 'True Positive': '#6C92ED', 5 'False Positive': '#F8B274', 6 'True Negative': '#A8C4A2', 7 'False Negative': '#E89B7A', 8}
Output Requirements
CRITICAL: Size Changes Mean GRAPH Size, Not Image Canvas
When the user asks to change figure dimensions (e.g., "make it smaller", "1.7 inches"), they mean the ACTUAL GRAPH/PLOT size, not the overall image canvas.
DO NOT:
- Shrink the graph while keeping the image canvas the same size (creates wasted whitespace)
- Add padding/margins to achieve a target image size
- Scale down the plot area within a larger figure
DO:
- Change
figsize=(width, height)to directly control the plot dimensions - The graph should fill the figure with minimal margins
- Use
bbox_inches='tight'when saving to remove excess whitespace
python1# CORRECT: figsize controls the actual graph size 2fig, ax = plt.subplots(figsize=(1.7, 1.7)) # Graph is 1.7" x 1.7" 3plt.savefig('output.png', dpi=600, bbox_inches='tight') 4 5# WRONG: Large canvas with small graph inside 6fig, ax = plt.subplots(figsize=(4, 4)) # 4" canvas 7ax.set_position([0.3, 0.3, 0.4, 0.4]) # Graph only 1.6" - DON'T DO THIS
The figsize parameter IS the graph size (plus minimal axis labels/title). When asked for "1.7 inch square", use figsize=(1.7, 1.7).
Always Save Both Formats
python1# Save PDF for LaTeX/publication 2plt.savefig('Output/path/figures/figure_name.pdf', bbox_inches='tight') 3# Save PNG at 600 DPI for high-quality viewing 4plt.savefig('Output/path/Figure_Name.png', dpi=600, bbox_inches='tight') 5plt.close()
Standard Figure Sizes
python1# Single panel 2fig, ax = plt.subplots(figsize=(8, 6)) 3 4# Side-by-side panels 5fig, axes = plt.subplots(1, 2, figsize=(12, 5)) 6 7# Three panels horizontal 8fig, axes = plt.subplots(1, 3, figsize=(14, 4)) 9 10# Grid layout 11fig, axes = plt.subplots(2, 2, figsize=(12, 10)) 12 13# Wide scatter plot (per-drug) 14fig, ax = plt.subplots(figsize=(16, 6)) 15 16# Default square graph (for bar charts, scatter plots, Accuracy vs AUC, etc.) 17SQUARE_SIZE = 1.7 # inches - standard square panel 18fig, ax = plt.subplots(figsize=(SQUARE_SIZE, SQUARE_SIZE)) 19 20# Heatmap (MANDATORY size - 1:2 ratio) 21HEATMAP_HEIGHT = 1.7 # inches 22HEATMAP_WIDTH = 3.4 # inches (2x height) 23fig, ax = plt.subplots(figsize=(HEATMAP_WIDTH, HEATMAP_HEIGHT)) 24 25# Accuracy vs AUC scatter (use square size, same as bar charts) 26fig, ax = plt.subplots(figsize=(1.7, 1.7)) # 1:1 square 27# Or for 3-panel comparison with shared axis: 28fig, axes = plt.subplots(1, 3, figsize=(5.1, 1.7), sharey=True) # 3 × 1.7" width 29 30# Bar chart (use square size) 31fig, ax = plt.subplots(figsize=(1.7, 1.7)) # Standard square
Common Figure Templates
1. Grouped Bar Chart (Accuracy, F1, MCC) with Error Bars
python1import numpy as np 2import matplotlib.pyplot as plt 3 4models = ['Model A', 'Model B', 'Model C'] 5metrics = ['Accuracy', 'F1 Score', 'MCC'] 6# Mean values 7data = np.array([ 8 [0.56, 0.72, 0.00], # Model A 9 [0.68, 0.73, 0.34], # Model B 10 [0.74, 0.77, 0.46], # Model C 11]) 12# Standard deviations (ALWAYS include error bars) 13data_std = np.array([ 14 [0.05, 0.04, 0.00], # Model A 15 [0.03, 0.05, 0.08], # Model B 16 [0.04, 0.03, 0.06], # Model C 17]) 18 19x = np.arange(len(models)) 20width = 0.25 21colors = ['#6C92ED', '#F8B274', '#ECA0C0'] # Use project color palette 22 23# Use 1.7" square for single bar charts (or scale up for grouped) 24SQUARE_SIZE = 1.7 25fig, ax = plt.subplots(figsize=(SQUARE_SIZE * 2, SQUARE_SIZE)) # 2:1 ratio for grouped 26for i, (metric, color) in enumerate(zip(metrics, colors)): 27 bars = ax.bar(x + i*width - width, data[:, i], width, 28 yerr=data_std[:, i], capsize=4, # Error bars with caps 29 label=metric, color=color, edgecolor='black') 30 # Add value labels above error bars 31 for j, bar in enumerate(bars): 32 height = bar.get_height() 33 err = data_std[j, i] 34 ax.annotate(f'{height:.2f}', 35 xy=(bar.get_x() + bar.get_width()/2, height + err), 36 xytext=(0, 3), textcoords='offset points', 37 ha='center', va='bottom', fontsize=9, fontweight='bold') 38 39ax.set_ylabel('Score', fontsize=9) 40ax.set_title('Performance Comparison', fontsize=10, fontweight='bold') 41ax.set_xticks(x) 42ax.set_xticklabels(models, fontsize=8) 43ax.tick_params(axis='both', labelsize=8) 44ax.legend(loc='upper left', fontsize=8) 45ax.set_ylim(0, 1.1) # Extra space for error bars 46ax.grid(axis='y', alpha=0.3) 47plt.tight_layout()
2. ROC Curve with Bootstrap Confidence Bands (MANDATORY)
CRITICAL: ALL ROC curves MUST include shaded confidence bands. ROC curves without shaded uncertainty regions are NOT acceptable for publication figures.
What the shaded band represents:
- The band shows ±1 standard deviation of TPR at each FPR point
- Computed via bootstrap resampling (default: 300 iterations)
- Wider bands = more uncertainty, narrower bands = more confidence
- Band is clamped to [0, 1] range (valid probability bounds)
python1import figure_config # FIRST LINE - registers Helvetica 2from sklearn.metrics import roc_curve, auc 3import numpy as np 4import matplotlib.pyplot as plt 5 6def bootstrap_roc_stats(y_true, y_prob, n_boot=300, seed=42): 7 """ 8 Bootstrap ROC statistics for confidence intervals. 9 10 Returns: 11 mean_fpr: Common FPR points (0 to 1, 100 points) 12 mean_tpr: Mean TPR at each FPR point 13 std_tpr: Standard deviation of TPR (for shaded band) 14 auc_mean: Mean AUC across bootstrap samples 15 auc_std: Standard deviation of AUC 16 """ 17 rng = np.random.default_rng(seed) 18 y_true, y_prob = np.asarray(y_true), np.asarray(y_prob) 19 n = len(y_true) 20 mean_fpr = np.linspace(0, 1, 100) 21 tprs, aucs = [], [] 22 23 for _ in range(n_boot): 24 idx = rng.integers(0, n, n) 25 if len(np.unique(y_true[idx])) < 2: 26 continue 27 fpr, tpr, _ = roc_curve(y_true[idx], y_prob[idx]) 28 tpr_interp = np.interp(mean_fpr, fpr, tpr) 29 tpr_interp[0] = 0.0 30 tprs.append(tpr_interp) 31 aucs.append(auc(mean_fpr, tpr_interp)) 32 33 tprs = np.array(tprs) 34 return mean_fpr, tprs.mean(axis=0), tprs.std(axis=0), np.mean(aucs), np.std(aucs) 35 36def plot_roc_with_bands(ax, mean_fpr, mean_tpr, std_tpr, auc_val, auc_std, color, label): 37 """ 38 Plot ROC curve with MANDATORY shaded confidence band. 39 40 The shaded region represents ±1 std of TPR at each FPR point, 41 showing the uncertainty from bootstrap resampling. 42 """ 43 # Plot the mean ROC curve 44 ax.plot(mean_fpr, mean_tpr, color=color, lw=2, 45 label=f'{label} (AUC={auc_val:.2f}±{auc_std:.2f})') 46 47 # MANDATORY: Shaded confidence band between upper and lower bounds 48 lower_bound = np.maximum(mean_tpr - std_tpr, 0) # Clamp to 0 minimum 49 upper_bound = np.minimum(mean_tpr + std_tpr, 1) # Clamp to 1 maximum 50 51 ax.fill_between( 52 mean_fpr, # X values (FPR points) 53 lower_bound, # Lower edge of shaded region 54 upper_bound, # Upper edge of shaded region 55 color=color, 56 alpha=0.2, # Semi-transparent shading 57 edgecolor='none' # No edge line on the shaded region 58 ) 59 60# COMPLETE USAGE EXAMPLE 61fig, ax = plt.subplots(figsize=(7, 6)) 62 63# Example: Plot ROC for multiple models 64models_data = [ 65 ('Model A', y_true_a, y_prob_a, '#6C92ED'), # Blue 66 ('Model B', y_true_b, y_prob_b, '#ECA0C0'), # Pink 67 ('Model C', y_true_c, y_prob_c, '#F8B274'), # Orange 68] 69 70for label, y_true, y_prob, color in models_data: 71 # Compute bootstrap statistics 72 mean_fpr, mean_tpr, std_tpr, auc_val, auc_std = bootstrap_roc_stats(y_true, y_prob) 73 # Plot with MANDATORY shaded band 74 plot_roc_with_bands(ax, mean_fpr, mean_tpr, std_tpr, auc_val, auc_std, color, label) 75 76# Random classifier baseline (diagonal) 77ax.plot([0, 1], [0, 1], 'k--', lw=1, label='Random (AUC=0.50)') 78 79ax.set_xlabel('False Positive Rate', fontsize=9) 80ax.set_ylabel('True Positive Rate', fontsize=9) 81ax.set_title('ROC Curves with Confidence Bands', fontsize=10, fontweight='bold') 82ax.tick_params(axis='both', labelsize=8) 83ax.legend(loc='lower right', fontsize=8) 84ax.set_xlim([0, 1]) 85ax.set_ylim([0, 1]) 86ax.grid(alpha=0.3) 87plt.tight_layout() 88 89# Save both formats 90plt.savefig('roc_curves.pdf', bbox_inches='tight') 91plt.savefig('roc_curves.png', dpi=600, bbox_inches='tight') 92plt.close()
Visual representation of shaded band:
TPR
1 ┤ ╭───────── Upper bound (mean + std)
│ ╭──┤░░░░░░░░░
│ ╭──┤░░░░░░░░░░░│ ← Shaded region shows uncertainty
│ ╭──┤░░░░░░░░░░░░░│
│ ╭──┤░░░░░░░░░░░░░░░─╯
│ ╭──┤░░░░░░░░░░░░░░░╯ ← Mean ROC curve (solid line)
│ ╭──┤░░░░░░░░░░░░░╯
│╭─┤░░░░░░░░░░░░╯ Lower bound (mean - std)
0 ┼──────────────────────────────
0 1 FPR
3. Confusion Matrix with Percentages
CRITICAL Sizing Rules:
- Use square figure size (
SQUARE_SIZE= 1.7") - Use
aspect='equal'inimshow()to force square cells - Margins:
left=0.18, right=0.98, top=0.85, bottom=0.18to maximize plot area - DO NOT use
set_box_aspect(1)- it constrains plot size and wastes space
python1import figure_config # FIRST LINE - registers Helvetica 2import numpy as np 3import matplotlib.pyplot as plt 4 5SQUARE_SIZE = 1.7 # Standard square panel size 6 7def plot_confusion_matrix(cm, labels, title, output_path=None): 8 """ 9 Plot confusion matrix with counts and row percentages. 10 Uses square cells that fill the figure properly. 11 """ 12 cm = np.asarray(cm, dtype=float) 13 row_sums = cm.sum(axis=1, keepdims=True) 14 row_sums[row_sums == 0] = 1.0 15 row_pct = cm / row_sums 16 17 # Square figure with tight margins to maximize plot area 18 fig, ax = plt.subplots(figsize=(SQUARE_SIZE, SQUARE_SIZE)) 19 fig.subplots_adjust(left=0.18, right=0.98, top=0.85, bottom=0.18) 20 21 # CRITICAL: Use aspect='equal' for square cells (NOT set_box_aspect) 22 im = ax.imshow(cm, cmap='Blues', aspect='equal') 23 24 ax.set_title(title, fontsize=10, fontweight='bold') 25 ax.set_xticks(np.arange(len(labels))) 26 ax.set_yticks(np.arange(len(labels))) 27 ax.set_xticklabels(labels, fontsize=8) 28 ax.set_yticklabels(labels, fontsize=8) 29 ax.set_xlabel('Predicted', fontsize=9) 30 ax.set_ylabel('Actual', fontsize=9) 31 32 max_val = cm.max() if cm.size else 0 33 for i in range(cm.shape[0]): 34 for j in range(cm.shape[1]): 35 count = int(cm[i, j]) 36 pct = row_pct[i, j] * 100 37 color = 'white' if max_val > 0 and cm[i, j] > max_val / 2 else 'black' 38 ax.text(j, i, f"{count}\n{pct:.1f}%", 39 ha='center', va='center', color=color, fontsize=9) 40 41 if output_path: 42 plt.savefig(output_path, dpi=600, bbox_inches='tight') 43 plt.close() 44 return fig, ax, im
4. Per-Drug Scatter Plot
python1fig, ax = plt.subplots(figsize=(16, 6)) 2x_pos = np.arange(len(drugs)) 3 4# Plot each model with offset 5for i, (model, probs, color, marker) in enumerate(model_data): 6 offset = (i - 1) * 0.15 7 mask = true_labels.astype(bool) 8 colors_arr = np.where(mask, color, 'lightgray') 9 ax.scatter(x_pos + offset, probs, s=100, c=colors_arr, 10 marker=marker, edgecolor='black', linewidth=1.5, 11 zorder=3, label=model) 12 13ax.axhline(0.5, color='red', linestyle='--', linewidth=2, alpha=0.7) 14ax.set_ylabel('Probability', fontsize=9) 15ax.set_xlabel('Drug', fontsize=9) 16ax.set_xticks(x_pos) 17ax.set_xticklabels(drugs, rotation=45, ha='right', fontsize=8) 18ax.tick_params(axis='both', labelsize=8) 19ax.set_ylim(-0.05, 1.15) 20ax.grid(axis='y', alpha=0.3) 21ax.legend(loc='upper right')
5. Heatmap (Red-White-Blue Diverging)
CRITICAL Heatmap Rules:
- Square cells - Always use
square=True, never rectangles - No borders/gaps - Always use
linewidths=0(no white space between cells) - Axis orientation - X-axis = Time (hours), Y-axis = Concentration (mM)
- Data matrix - Rows = concentrations, Columns = time points
- Clean labels - Remove duplicate decimal suffixes (8.1 → 8), keep meaningful ones (0.1 stays 0.1)
python1import figure_config # FIRST LINE - registers Helvetica 2import numpy as np 3import matplotlib.pyplot as plt 4import seaborn as sns 5import pandas as pd 6import re 7 8def clean_concentration_labels(labels): 9 """ 10 Clean concentration labels by removing duplicate decimal suffixes. 11 12 Examples: 13 '8.1' → '8' (removes .1 suffix that's just numbering) 14 '8.2' → '8' (removes .2 suffix) 15 '0.1' → '0.1' (keeps meaningful decimal - it's the actual value) 16 '0.1.1' → '0.1' (removes duplicate suffix from 0.1) 17 """ 18 cleaned = [] 19 for label in labels: 20 label_str = str(label) 21 # Pattern: number.number.number (like 0.1.1) → keep first two parts 22 if re.match(r'^\d+\.\d+\.\d+$', label_str): 23 parts = label_str.split('.') 24 cleaned.append(f"{parts[0]}.{parts[1]}") 25 # Pattern: integer.single_digit at end (like 8.1, 8.2) → remove suffix 26 elif re.match(r'^(\d+)\.[1-9]$', label_str): 27 cleaned.append(re.match(r'^(\d+)\.[1-9]$', label_str).group(1)) 28 else: 29 cleaned.append(label_str) 30 return cleaned 31 32# Example: Drug response heatmap 33# CRITICAL: Rows = concentrations (Y-axis), Columns = time (X-axis) 34n_concentrations = 8 35n_timepoints = 40 36 37data_matrix = pd.DataFrame( 38 np.random.randn(n_concentrations, n_timepoints), 39 index=[f'{c}' for c in [0, 0.1, 1, 2, 4, 8, 16, 32]], # Concentration labels 40 columns=[f'{t}' for t in range(0, n_timepoints * 2, 2)] # Time labels (hours) 41) 42 43# Clean concentration labels (Y-axis) 44y_labels = clean_concentration_labels(data_matrix.index.tolist()) 45x_labels = data_matrix.columns.tolist() # Time labels (X-axis) 46 47# Setup custom colormap with project colors 48from matplotlib.colors import LinearSegmentedColormap 49 50# Project heatmap colors (MANDATORY) 51HEATMAP_BLUE = '#123BFF' # Low values 52HEATMAP_RED = '#FF2908' # High values 53 54# Create custom diverging colormap: Blue -> White -> Red 55cmap = LinearSegmentedColormap.from_list( 56 'cardiac_rodeo', 57 [HEATMAP_BLUE, 'white', HEATMAP_RED] 58) 59cmap.set_bad('white') # NaN values display as white 60 61# MANDATORY Figure size for heatmaps (1:2 ratio) 62HEATMAP_HEIGHT = 1.7 # inches 63HEATMAP_WIDTH = 3.4 # inches (2x height) 64fig, ax = plt.subplots(figsize=(HEATMAP_WIDTH, HEATMAP_HEIGHT)) 65 66# Create heatmap with SQUARE cells and NO borders/gaps 67sns.heatmap( 68 data_matrix, 69 annot=False, # No cell annotations 70 cmap=cmap, 71 cbar_kws={'label': 'Response', 'shrink': 0.8}, 72 xticklabels=x_labels, # Time labels (X-axis) 73 yticklabels=y_labels, # Concentration labels (Y-axis) 74 square=True, # CRITICAL: Square cells, not rectangles 75 mask=False, 76 linewidths=0 # CRITICAL: No borders/gaps between cells 77) 78 79# Customize tick labels - FIXED SIZES 80ax.set_xticklabels(x_labels, rotation=0, ha='center', fontsize=8) 81ax.set_yticklabels(y_labels, fontsize=8, rotation=0) 82 83# CRITICAL: X = Time, Y = Concentration - FIXED FONT SIZES 84ax.set_xlabel('Time (Hours)', fontsize=9) 85ax.set_ylabel('Concentration (mM)', fontsize=9) 86ax.set_title('Drug Response Heatmap', fontsize=10, fontweight='bold') 87 88plt.tight_layout()
Heatmap Axis Orientation (MANDATORY):
X-axis: Time (Hours) →
┌─────────────────────────┐
Y │ 0 2 4 6 ... 78 │
a ├─────────────────────────┤
x │ 32 │ ■ │ ■ │ ■ │ │ ■ │
i │ 16 │ ■ │ ■ │ ■ │ │ ■ │
s │ 8 │ ■ │ ■ │ ■ │ │ ■ │
: │ 4 │ ■ │ ■ │ ■ │ │ ■ │
C │ 2 │ ■ │ ■ │ ■ │ │ ■ │
o │ 1 │ ■ │ ■ │ ■ │ │ ■ │
n │0.1 │ ■ │ ■ │ ■ │ │ ■ │
c │ 0 │ ■ │ ■ │ ■ │ │ ■ │
└─────────────────────────┘
Heatmap Colormap Reference:
| Data Type | Colormap | Setup | Notes |
|---|---|---|---|
| Drug response (DEFAULT) | Custom cardiac_rodeo | See code above | #123BFF (blue) → White → #FF2908 (red) |
| Correlation | Custom or RdBu_r | center=0, vmin=-1, vmax=1 | Symmetric around zero |
| Performance (R², accuracy) | RdYlGn | center=0 | Red (bad) → Yellow → Green (good) |
| Confusion Matrix | Blues | N/A | One-sided, counts only |
| SHAP values | coolwarm | N/A | Blue (neg) → White → Red (pos) |
MANDATORY Heatmap Colors:
python1HEATMAP_BLUE = '#123BFF' # For low values 2HEATMAP_RED = '#FF2908' # For high values
6. Threshold Analysis Scatter Plot (Horizontal)
python1import numpy as np 2import matplotlib.pyplot as plt 3import pandas as pd 4 5# Example data 6drugs = ['Amiodarone', 'Bortezomib', 'Chlorpromazine', 'Doxorubicin', 'Erlotinib', 7 'Ibuprofen', 'Nifedipine', 'Sotalol', 'Vincristine', 'Vioxx'] 8predictions = np.array([85, 72, 45, 90, 30, 25, 55, 78, 40, 65]) # Probability % 9actual_positive = np.array([True, True, False, True, False, False, True, True, False, True]) 10 11# Sort drugs: by classification (positive first), then alphabetically 12df = pd.DataFrame({'Drug': drugs, 'Pred': predictions, 'Positive': actual_positive}) 13df = df.sort_values(['Positive', 'Drug'], ascending=[False, True]) 14drugs_sorted = df['Drug'].tolist() 15preds_sorted = df['Pred'].values 16status_sorted = df['Positive'].values 17 18# Colors: Green = positive, Red = negative 19pos_color = '#2ca02c' # Green 20neg_color = '#d62728' # Red 21threshold_color = '#1f77b4' # Blue for threshold line 22 23# Create horizontal scatter plot (drugs on Y-axis) 24fig, ax = plt.subplots(figsize=(10, max(6, len(drugs) * 0.4))) 25 26positions = np.arange(len(drugs_sorted)) 27point_colors = [pos_color if s else neg_color for s in status_sorted] 28 29# Scatter: X = probability, Y = drug position 30ax.scatter(preds_sorted, positions, c=point_colors, s=100, 31 edgecolors='black', linewidth=0.5, zorder=3) 32 33# Compute threshold: max(negative samples) + margin, rounded to nearest 5 34margin_pp = 2.0 35neg_preds = preds_sorted[~status_sorted] 36if len(neg_preds) > 0: 37 threshold = float(np.max(neg_preds)) + margin_pp 38else: 39 threshold = 50.0 40threshold = float(5 * np.ceil(threshold / 5.0)) # Round up to nearest 5 41 42# Vertical threshold line 43ax.axvline(threshold, color=threshold_color, linestyle='--', linewidth=2, zorder=2) 44ax.text(threshold + 1, len(drugs_sorted) - 0.5, f'{threshold:.0f}%', 45 color=threshold_color, fontsize=10, fontweight='bold', va='top') 46 47# Axis setup with padding for 0% and 100% visibility 48ax.set_xlim(-5, 105) # Padding so edge points are visible 49ax.set_ylim(-0.5, len(drugs_sorted) - 0.5) 50 51ax.set_yticks(positions) 52ax.set_yticklabels(drugs_sorted, fontsize=8) 53ax.set_xlabel('Predicted Probability (%)', fontsize=9) 54ax.set_title('Threshold Analysis: Arrhythmia Prediction', fontsize=10, fontweight='bold') 55ax.tick_params(axis='both', labelsize=8) 56 57ax.grid(axis='x', linestyle='--', alpha=0.4) 58ax.invert_yaxis() # First drug at top 59 60# Legend 61from matplotlib.lines import Line2D 62legend_elements = [ 63 Line2D([0], [0], marker='o', color='w', markerfacecolor=pos_color, 64 markersize=10, label='Positive (Actual)'), 65 Line2D([0], [0], marker='o', color='w', markerfacecolor=neg_color, 66 markersize=10, label='Negative (Actual)'), 67 Line2D([0], [0], color=threshold_color, linestyle='--', linewidth=2, label='Threshold') 68] 69ax.legend(handles=legend_elements, loc='lower right', fontsize=8) 70 71plt.tight_layout()
Key Features:
- Drugs on Y-axis for readable labels
- X-axis: 0-100% with
-5, 105limits for edge visibility - Green = positive class, Red = negative class
- Sorted by classification (positive first) then alphabetically
- Threshold = max(negative) + margin, rounded to nearest 5%
- Vertical threshold line with
axvline
7. 3D Surface Plot (PK-PD)
python1from mpl_toolkits.mplot3d import Axes3D 2import numpy as np 3 4fig = plt.figure(figsize=(10, 8)) 5ax = fig.add_subplot(111, projection='3d') 6 7# Create meshgrid 8time = np.linspace(0, 96, 50) 9dose_ratio = np.linspace(0, 2, 50) 10T, Dr = np.meshgrid(time, dose_ratio) 11Response = compute_response(T, Dr, params) # Your function 12 13surf = ax.plot_surface(T, Dr, Response, cmap='viridis', 14 edgecolor='none', alpha=0.9) 15ax.set_xlabel('Time (hours)', fontsize=9) 16ax.set_ylabel('Dose Ratio (C0/Cmax)', fontsize=9) 17ax.set_zlabel('Response', fontsize=9) 18ax.set_title('Drug Response Surface', fontsize=10, fontweight='bold') 19ax.tick_params(axis='both', labelsize=8) 20ax.view_init(elev=25, azim=-158) 21fig.colorbar(surf, shrink=0.5, aspect=10)
8. SHAP Aligned Pairs Plot
python1import figure_config # FIRST LINE - registers Helvetica 2import numpy as np 3import matplotlib.pyplot as plt 4import pandas as pd 5from matplotlib.lines import Line2D 6 7# Load SHAP values (example: arrhythmia) 8shap_df = pd.read_csv('Output/SHAP_Data/shap_arrhythmia_values.csv') 9drug_class = pd.read_csv('Cleaned_Data/drug_classification.csv') 10 11# Create label map: drug -> actual class (True/False) 12label_map = {} 13for _, row in drug_class.iterrows(): 14 drug = row['Drug'] 15 val = row['Arrhythmia'] # or 'heart_damage', 'Concern', etc. 16 label_map[drug] = str(val).lower() == 'true' if isinstance(val, str) else bool(val) 17 18# Colors: Blue for positive class, Grey for negative class 19COLORS = { 20 'pass': '#6C92ED', # Blue - positive class 21 'fail': '#888888', # Grey - negative class 22} 23 24# Get feature columns and select top 5 by |mean SHAP| 25feature_cols = [col for col in shap_df.columns if col != 'Drug'] 26mean_shap = shap_df[feature_cols].abs().mean() 27top_5_features = mean_shap.nlargest(5).index.tolist() 28 29# Figure size: 2x width for SHAP plots 30SQUARE_SIZE = 1.7 31fig, ax = plt.subplots(figsize=(SQUARE_SIZE * 2, SQUARE_SIZE)) 32fig.subplots_adjust(left=0.25, right=0.85, top=0.88, bottom=0.12) 33 34n_features = len(top_5_features) 35n_drugs = len(shap_df) 36feature_spacing = 1.0 37drug_offset = 0.03 38 39y_positions = [] 40y_labels = [] 41 42for feat_idx, feature in enumerate(reversed(top_5_features)): 43 base_y = feat_idx * feature_spacing 44 y_positions.append(base_y) 45 y_labels.append(feature) 46 47 # Sort drugs by SHAP value for this feature 48 sorted_idx = np.argsort(shap_df[feature].values) 49 50 for i, idx in enumerate(sorted_idx): 51 drug = shap_df['Drug'].iloc[idx] 52 val = shap_df[feature].iloc[idx] 53 y = base_y + (i - n_drugs/2) * drug_offset 54 55 # Color by actual class 56 is_positive = label_map.get(drug, False) 57 color = COLORS['pass'] if is_positive else COLORS['fail'] 58 59 # Draw line from 0 to SHAP value 60 ax.hlines(y, 0, val, colors=color, linewidth=1.2, alpha=0.8) 61 62ax.axvline(x=0, color='black', linewidth=0.8) 63ax.set_yticks(y_positions) 64ax.set_yticklabels(y_labels, fontsize=8) 65ax.set_xlabel('SHAP Value', fontsize=9) 66ax.set_title('SHAP Feature Importance', fontsize=10, fontweight='bold') 67ax.spines['top'].set_visible(False) 68ax.spines['right'].set_visible(False) 69ax.grid(axis='x', alpha=0.3) 70ax.tick_params(axis='x', labelsize=8) 71 72legend_elements = [ 73 Line2D([0], [0], color=COLORS['pass'], linewidth=2, label='Pos'), 74 Line2D([0], [0], color=COLORS['fail'], linewidth=2, label='Neg'), 75] 76ax.legend(handles=legend_elements, fontsize=8, loc='upper right') 77 78plt.savefig('Output/SHAP_Data/shap_aligned_target.png', dpi=600) 79plt.close()
Key Features:
- Shows top 5 features by |mean SHAP|
- For each feature, all drugs are plotted as horizontal lines from 0 to their SHAP value
- Lines sorted by SHAP value within each feature group
- Color indicates actual class (not SHAP sign): blue=positive, grey=negative
- Simple, clean visualization without complex pairing logic
- Compact size (3.4" × 1.7") for PowerPoint integration
Data Export Pattern (for Excel recreation):
python1excel_data = [] 2for feature, y_pos, shap_val, drug, actual in plot_records: 3 excel_data.append({ 4 'Feature': feature, 5 'Y_Position': y_pos, 6 'SHAP_Value': shap_val, 7 'Drug': drug, 8 'Actual_Class': 'Positive' if actual else 'Negative' 9 }) 10pd.DataFrame(excel_data).to_excel('shap_aligned_pairs_data.xlsx', index=False)
9. Accuracy vs AUC Scatter Plot (Equation/Model Comparison)
NOT a bar chart! This is a scatter plot comparing equations across ML models.
- X-axis = Accuracy
- Y-axis = AUC
- Each point = one (equation, model) combination
- Color/marker by equation, grouped by model
python1import figure_config # FIRST LINE - registers Helvetica 2import numpy as np 3import matplotlib.pyplot as plt 4import pandas as pd 5 6# Example data: 3 equations × 3 ML models = 9 points 7equations = ['dual_exponential', 'modified_hill_hormesis', 'pkpd_elimination'] 8models = ['XGBoost', 'SVM_RBF', 'RandomForest'] 9 10# Simulated results (replace with actual loocv_results.csv data) 11data = { 12 ('dual_exponential', 'XGBoost'): {'Accuracy': 0.72, 'AUC': 0.78}, 13 ('dual_exponential', 'SVM_RBF'): {'Accuracy': 0.68, 'AUC': 0.72}, 14 ('dual_exponential', 'RandomForest'): {'Accuracy': 0.70, 'AUC': 0.75}, 15 ('modified_hill_hormesis', 'XGBoost'): {'Accuracy': 0.76, 'AUC': 0.82}, 16 ('modified_hill_hormesis', 'SVM_RBF'): {'Accuracy': 0.74, 'AUC': 0.80}, 17 ('modified_hill_hormesis', 'RandomForest'): {'Accuracy': 0.73, 'AUC': 0.78}, 18 ('pkpd_elimination', 'XGBoost'): {'Accuracy': 0.80, 'AUC': 0.86}, 19 ('pkpd_elimination', 'SVM_RBF'): {'Accuracy': 0.78, 'AUC': 0.84}, 20 ('pkpd_elimination', 'RandomForest'): {'Accuracy': 0.77, 'AUC': 0.82}, 21} 22 23# Colors by equation 24equation_colors = { 25 'dual_exponential': '#e74c3c', # Red 26 'modified_hill_hormesis': '#3498db', # Blue 27 'pkpd_elimination': '#2ecc71', # Green 28} 29 30# Markers by model 31model_markers = { 32 'XGBoost': 'o', # Circle 33 'SVM_RBF': 's', # Square 34 'RandomForest': '^', # Triangle 35} 36 37# Use 1.7" square (same as bar charts) 38SQUARE_SIZE = 1.7 39fig, ax = plt.subplots(figsize=(SQUARE_SIZE, SQUARE_SIZE)) 40 41# Plot each point 42for (eq, model), metrics in data.items(): 43 ax.scatter( 44 metrics['Accuracy'], metrics['AUC'], 45 c=equation_colors[eq], 46 marker=model_markers[model], 47 s=40, # Appropriate markers for 1.7" square 48 edgecolors='black', 49 linewidth=1, 50 label=f'{eq} / {model}' if model == models[0] else '', # Only label once per equation 51 zorder=3 52 ) 53 54# Reference lines 55ax.axhline(0.5, color='gray', linestyle='--', alpha=0.5, linewidth=1) # AUC baseline 56ax.axvline(0.5, color='gray', linestyle='--', alpha=0.5, linewidth=1) # Accuracy baseline 57 58# Axis settings - FIXED FONT SIZES 59ax.set_xlabel('Accuracy', fontsize=9) 60ax.set_ylabel('AUC', fontsize=9) 61ax.set_title('Equation Comparison: Accuracy vs AUC', fontsize=10, fontweight='bold') 62ax.tick_params(axis='both', labelsize=8) 63ax.set_xlim(0.4, 1.0) 64ax.set_ylim(0.4, 1.0) 65ax.set_aspect('equal') # Square plot for equal scaling 66ax.grid(True, alpha=0.3) 67 68# Custom legend 69from matplotlib.lines import Line2D 70from matplotlib.patches import Patch 71 72# Equation colors legend 73eq_legend = [Patch(facecolor=c, edgecolor='black', label=eq.replace('_', ' ').title()) 74 for eq, c in equation_colors.items()] 75 76# Model markers legend 77model_legend = [Line2D([0], [0], marker=m, color='w', markerfacecolor='gray', 78 markersize=10, label=model, markeredgecolor='black') 79 for model, m in model_markers.items()] 80 81legend1 = ax.legend(handles=eq_legend, title='Equation', loc='upper left', fontsize=8) 82ax.add_artist(legend1) 83ax.legend(handles=model_legend, title='Model', loc='lower right', fontsize=8) 84 85plt.tight_layout()
Key Features:
- X = Accuracy, Y = AUC (NOT bar chart)
- Each point represents one (equation, model) combination
- Color distinguishes equations
- Marker shape distinguishes ML models
- Square plot with equal scaling for fair comparison
- Reference lines at 0.5 for both axes (random baseline)
Space-Saving Rules for Multi-Panel Figures
CRITICAL: Shared Axes for Same-Scale Graphs
When multiple graphs share the same axis scale, place them side-by-side and show only ONE axis instead of repeating.
This eliminates wasted space from duplicate axis labels and tick marks.
python1import figure_config 2import matplotlib.pyplot as plt 3import matplotlib.gridspec as gridspec 4 5# WRONG: Each subplot has its own Y-axis (wastes space) 6fig, axes = plt.subplots(1, 3, figsize=(9, 3)) 7for ax in axes: 8 ax.set_ylabel('Response') # Repeated 3 times! 9 10# CORRECT: Share Y-axis, only label the leftmost 11fig, axes = plt.subplots(1, 3, figsize=(7, 3), sharey=True) 12axes[0].set_ylabel('Response') # Only once on the left 13for ax in axes[1:]: 14 ax.tick_params(labelleft=False) # Hide Y tick labels on others 15 16# BEST: Use GridSpec for tight control and minimal gaps 17fig = plt.figure(figsize=(5.19, 1.44)) # 3 heatmaps side-by-side 18gs = gridspec.GridSpec(1, 3, wspace=0.05) # Minimal horizontal gap 19 20axes = [fig.add_subplot(gs[0, i]) for i in range(3)] 21axes[0].set_ylabel('Concentration (mM)') # Only on leftmost 22for ax in axes[1:]: 23 ax.set_yticklabels([]) # No Y labels on middle/right panels
Visual comparison:
WRONG (wasted space): CORRECT (shared axis):
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────────────┐
│ Y │ │ Y │ │ Y │ │ Y │
│ │ ■■■ │ │ │ ■■■ │ │ │ ■■■ │ │ │ ■■■ │ ■■■ │ ■■■ │
│ │ ■■■ │ │ │ ■■■ │ │ │ ■■■ │ │ │ ■■■ │ ■■■ │ ■■■ │
│ └──X │ │ └──X │ │ └──X │ │ └──────────────X │
└─────────┘ └─────────┘ └─────────┘ └─────────────────────────┘
Drug A Drug B Drug C Drug A Drug B Drug C
CRITICAL: Shared Legends and Colorbars
When multiple graphs in the same panel use the same color scale or legend, show only ONE colorbar/legend for all of them.
python1import matplotlib.pyplot as plt 2import matplotlib.gridspec as gridspec 3import numpy as np 4 5# Example: 3 heatmaps with one shared colorbar 6fig = plt.figure(figsize=(5.5, 1.44)) 7 8# Create GridSpec: 3 heatmaps + 1 narrow colorbar column 9gs = gridspec.GridSpec(1, 4, width_ratios=[1, 1, 1, 0.05], wspace=0.1) 10 11# Determine shared vmin/vmax across all data 12all_data = [data1, data2, data3] 13vmin = min(d.min() for d in all_data) 14vmax = max(d.max() for d in all_data) 15 16# Plot heatmaps with same color scale 17for i, data in enumerate(all_data): 18 ax = fig.add_subplot(gs[0, i]) 19 im = ax.imshow(data, vmin=vmin, vmax=vmax, cmap='RdBu_r') 20 if i == 0: 21 ax.set_ylabel('Concentration') 22 else: 23 ax.set_yticklabels([]) # No Y labels on non-leftmost 24 25# Single colorbar for all three 26cbar_ax = fig.add_subplot(gs[0, 3]) 27fig.colorbar(im, cax=cbar_ax, label='Response')
Rules summary:
| Element | When to Share | How to Implement |
|---|---|---|
| Y-axis | Same units & range | sharey=True, label only leftmost |
| X-axis | Same units & range | sharex=True, label only bottom |
| Colorbar | Same color scale | Single colorbar() with cax= |
| Legend | Same categories | Single fig.legend() outside subplots |
Applying to Heatmaps
For multiple drug heatmaps (e.g., Contractility responses for different drugs):
python1import figure_config 2import matplotlib.pyplot as plt 3import matplotlib.gridspec as gridspec 4import seaborn as sns 5 6# Standard heatmap dimensions (1:2 ratio) 7HEATMAP_HEIGHT = 1.7 # inches 8HEATMAP_WIDTH = 3.4 # inches (2x height) 9 10n_drugs = 3 11fig = plt.figure(figsize=(HEATMAP_WIDTH * n_drugs + 0.3, HEATMAP_HEIGHT)) 12 13# GridSpec: n heatmaps + colorbar 14gs = gridspec.GridSpec(1, n_drugs + 1, 15 width_ratios=[1]*n_drugs + [0.05], 16 wspace=0.05) 17 18# Shared color scale 19vmin, vmax = 0, 100 # Or compute from data 20 21# Create custom colormap with project colors 22from matplotlib.colors import LinearSegmentedColormap 23HEATMAP_BLUE = '#123BFF' 24HEATMAP_RED = '#FF2908' 25cmap = LinearSegmentedColormap.from_list('cardiac_rodeo', [HEATMAP_BLUE, 'white', HEATMAP_RED]) 26 27for i, drug in enumerate(drugs): 28 ax = fig.add_subplot(gs[0, i]) 29 sns.heatmap(data[drug], ax=ax, vmin=vmin, vmax=vmax, 30 cmap=cmap, cbar=False, square=True) 31 ax.set_title(drug, fontsize=10) 32 33 if i == 0: 34 ax.set_ylabel('Concentration (mM)', fontsize=10) 35 else: 36 ax.set_ylabel('') 37 ax.set_yticklabels([]) 38 39 ax.set_xlabel('Time (h)', fontsize=10) 40 41# Single shared colorbar 42cbar_ax = fig.add_subplot(gs[0, -1]) 43sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin, vmax)) 44fig.colorbar(sm, cax=cbar_ax, label='Response') 45 46plt.tight_layout()
Style Guidelines
Font Settings (MANDATORY: Use Helvetica)
ALWAYS use Helvetica as the default font for all figures. No fallbacks.
CRITICAL: Add import figure_config as the FIRST line of any plotting code.
Font Sizes (MANDATORY: Fixed Sizes for ALL Text Elements)
CRITICAL: Use these EXACT font sizes for ALL text in ALL figures regardless of figure dimensions. This applies to EVERYTHING:
- Titles
- Axis labels (xlabel, ylabel)
- Tick labels
- Legend text
- Annotations
- Panel labels (a), (b), (c)
- Colorbar labels
Font sizes must be UNIFORM across all graphs - never scale fonts based on figure size.
How Font Sizes Work in Matplotlib
Font sizes are in POINTS (absolute units), where 1 point = 1/72 inch.
This means:
- A 10pt title is ALWAYS 10/72 = 0.139 inches tall when printed/saved
- A 9pt axis label is ALWAYS 9/72 = 0.125 inches tall
- An 8pt tick label is ALWAYS 8/72 = 0.111 inches tall
- This is true regardless of
figsize- fonts don't scale with the figure - When you save at 600 DPI, fonts render at their true point size
The result: A 1.7" figure and a 4" figure with the same font sizes will have ALL text (titles, axis labels, tick labels, legends, etc.) at the SAME PHYSICAL SIZE when printed. Text takes up MORE of the small figure's area, but is the same size in inches.
This is the desired behavior for publication consistency. When figures are placed side-by-side or in a grid, all text elements are the same readable size.
python1# MANDATORY FONT SIZE CONSTANTS - use these exact values everywhere 2FONT_SIZES = { 3 'title': 10, # Figure/panel titles (0.139") 4 'axis_label': 9, # X and Y axis labels (0.125") 5 'tick_label': 8, # Tick mark labels (0.111") 6 'legend': 8, # Legend text 7 'annotation': 8, # Text annotations on plots 8 'panel_label': 10, # Panel labels (a), (b), (c) 9 'colorbar_label': 8, # Colorbar labels 10} 11 12# Apply in every figure: 13ax.set_title('Title', fontsize=10, fontweight='bold') 14ax.set_xlabel('X Label', fontsize=9) 15ax.set_ylabel('Y Label', fontsize=9) 16ax.tick_params(axis='both', labelsize=8) 17ax.legend(fontsize=8)
Why This Ensures Consistency
When you have multiple figures of different sizes:
- 1.7" square confusion matrix
- 3.4" wide ROC curve
- 6" wide per-drug scatter
All will have 10pt titles that are physically the same size. This means:
- Readers can compare figures without adjusting to different text sizes
- Printed/PDF figures look professional and consistent
- PowerPoint slides have uniform text regardless of figure dimensions
DO NOT do this:
python1# WRONG - scaling font based on figure size 2fontsize = fig.get_figwidth() * 2 # NO! 3ax.set_title('Title', fontsize=14) # NO - not standard 4ax.set_xlabel('Label', fontsize=12) # NO - not standard 5 6# WRONG - trying to make text "fit" smaller figures 7small_fig_fontsize = 6 # NO - will be unreadable and inconsistent
DO this:
python1# CORRECT - fixed sizes from standard (same for ALL figures) 2ax.set_title('Title', fontsize=10, fontweight='bold') # Always 10pt 3ax.set_xlabel('Label', fontsize=9) # Always 9pt 4ax.tick_params(labelsize=8) # Always 8pt 5 6# This applies to 1.7" figures AND 6" figures - same font sizes
Ensuring Fonts Look Consistent When Viewing
Since fonts are absolute, a 1.7" figure will have relatively larger text (more of the figure area is text). To verify consistency:
- Save at consistent DPI (600) - ensures font rendering matches
- View figures at 100% zoom - see actual print size
- Never resize figures non-proportionally - stretching changes aspect but not fonts
python1import figure_config # LINE 1 - registers Helvetica from fonts/ folder 2import matplotlib.pyplot as plt 3import numpy as np 4 5# Now plot - Helvetica is already configured 6fig, ax = plt.subplots(figsize=(8, 6)) 7# ... your plotting code
The figure_config.py module (located at project root) automatically:
- Registers all fonts from the
fonts/directory - Sets Helvetica as the default font family
- Configures standard font sizes and weights
For inline/ad-hoc code without figure_config:
python1from pathlib import Path 2import matplotlib.font_manager as fm 3import matplotlib.pyplot as plt 4 5# Register Helvetica from fonts/ folder 6font_dir = Path(r'C:\Users\NoahB\Documents\HebrewU Bioengineering\Cardiac_RODEO\fonts') 7for font_file in font_dir.glob('*.ttf'): 8 fm.fontManager.addfont(str(font_file)) 9plt.rcParams['font.family'] = 'sans-serif' 10plt.rcParams['font.sans-serif'] = ['Helvetica']
Always Include
- Clear axis labels with units where applicable
- Title with fontweight='bold'
- Grid with alpha=0.3 for readability
- Legend in appropriate location
- tight_layout() before saving
Directory Structure
Output/
├── MoLFormer_Comparison/
│ ├── figures/ # PDF versions
│ │ ├── accuracy_bar.pdf
│ │ ├── roc_curves_all.pdf
│ │ └── confusion_matrices_all.pdf
│ ├── Accuracy_Bar.png # PNG versions (capitalized)
│ └── ROC_Curves_All.png
├── ADMET_Comparison/
│ └── figures/
└── LaTeX_Reports/
└── figures/ # Symlink or copy PDFs here
Checklist Before Saving
-
import figure_configis the first line (registers Helvetica) - Font sizes are UNIFORM (title=10, axis_label=9, tick_label=8, legend=8)
- Title is descriptive and bold
- Axis labels include units if applicable
- Legend doesn't obscure data
- Color scheme is consistent with project
- Saved as both PDF and PNG
- tight_layout() called
- Figure closed with plt.close()
- ROC curves: Shaded confidence band included (use
fill_betweenwith bootstrap ±1 std) - Heatmaps: No borders/gaps between cells (use
linewidths=0,square=True)
PowerPoint Figure System
This section defines the conventions for creating figures intended for PowerPoint presentations and publication, with full traceability and consistency.
CRITICAL: Automatic PowerPoint Insertion
When generating or modifying a tracked figure, you MUST automatically insert it into the target PowerPoint file.
This is NOT optional. After saving the PNG/PDF and Excel files, the figure must be placed into the PowerPoint at its designated location with proper sizing and labels.
Target file: Output/PowerPoint_Figures/Cardiac_RODEO_Tracked.pptx
CRITICAL: Link Images, Don't Embed
Always LINK images to PowerPoint instead of embedding them. This enables automatic updates when figures are regenerated.
Why link instead of embed:
- When you regenerate a figure with Python, the PowerPoint updates automatically
- No need to manually delete and re-insert figures
- Keeps file size smaller (images stored once on disk)
- Enables rapid iteration: edit Python → run script → switch to PowerPoint → see updated figure
Workflow:
- Python script saves figure to a fixed path (e.g.,
Output/PowerPoint_Figures/Fig_2/Fig_2_a_ROC.png) - PowerPoint links to that exact path
- When you re-run the script, the PNG is overwritten
- PowerPoint automatically shows the new version (may need to refresh/reopen)
python-pptx: Link instead of embed:
python1from pptx import Presentation 2from pptx.util import Inches 3from pptx.oxml.ns import qn 4from pptx.oxml import parse_xml 5from lxml import etree 6 7def add_linked_picture(slide, image_path, left, top, width=None, height=None): 8 """ 9 Add a picture to a slide as a LINKED image (not embedded). 10 The image will update automatically when the source file changes. 11 12 Args: 13 slide: pptx slide object 14 image_path: Absolute path to the image file 15 left, top: Position in Inches 16 width, height: Size in Inches (optional, preserves aspect ratio if only one given) 17 """ 18 from pptx.util import Emu 19 from PIL import Image 20 import os 21 22 # Get image dimensions for aspect ratio 23 with Image.open(image_path) as img: 24 img_width, img_height = img.size 25 aspect = img_width / img_height 26 27 # Calculate size 28 if width and not height: 29 height = width / aspect 30 elif height and not width: 31 width = height * aspect 32 elif not width and not height: 33 width = Inches(4) # default 34 height = width / aspect 35 36 # Add picture shape (this embeds, but we'll convert to link) 37 picture = slide.shapes.add_picture( 38 image_path, 39 Inches(left), Inches(top), 40 Inches(width), Inches(height) 41 ) 42 43 # To make it a TRUE linked picture, you need to manually edit the .pptx 44 # after creation, or use the method below to store the path for reference 45 46 # Store the source path in the shape's name for tracking 47 picture.name = f"LINKED:{image_path}" 48 49 return picture 50 51# For true linking, use Insert > Picture > Link to File in PowerPoint GUI 52# python-pptx doesn't natively support linked pictures, but you can: 53# 1. Use the GUI to insert linked pictures initially 54# 2. Use python-pptx only for positioning/sizing 55# 3. Or manually edit the XML (advanced)
Recommended workflow for linked images:
- Initial setup (GUI): Insert pictures using PowerPoint's "Insert → Pictures → This Device" then click the dropdown arrow on "Insert" and select "Link to File"
- Subsequent updates: Just re-run your Python scripts - the linked images update automatically
- Tracking: Keep
figure_registry.csvupdated with exact file paths
PowerPoint GUI steps to link an image:
- Insert → Pictures → This Device
- Navigate to your figure (e.g.,
Output/PowerPoint_Figures/Fig_2/Fig_2_a_ROC.png) - Click the dropdown arrow next to "Insert" button
- Select "Link to File" (not "Insert")
- The image is now linked - it will update when the source file changes
Slide Dimensions (MANDATORY)
python1SLIDE_WIDTH = 7.09 # inches 2SLIDE_HEIGHT = 8.47 # inches (portrait orientation) 3MARGIN = 0.5 # inches from edges 4GAP = 0.15 # inches between figures
Figure Hierarchy
Structure:
- Figure number (1, 2, 3...) = Main figure, gets its own slide
- Panel letter (a, b, c...) = Subfigure boxes within that figure
- Images = One or more graphics inside each panel box
CRITICAL: Horizontal-First Panel Filling Panels fill horizontally first (left to right), then wrap to next row:
- Panels a, b fill the first row side-by-side
- If there's room, panel c goes next to b
- If not, panel c starts a new row below
- Never stack panels vertically if horizontal space is available
CORRECT (horizontal-first): WRONG (vertical stacking):
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Figure 3 │ │ Figure 3 │
├──────────────────────────────┤ ├──────────────────────────────┤
│ ┌──────────┐ ┌──────────┐ │ │ ┌──────────────────────┐ │
│ │ (a) ROC │ │ (b) CM │ │ │ │ (a) ROC │ │
│ └──────────┘ └──────────┘ │ │ └──────────────────────┘ │
│ ┌──────────┐ │ │ ┌──────────────────────┐ │
│ │ (c) SHAP │ │ │ │ (b) Confusion Matrix │ │
│ └──────────┘ │ │ └──────────────────────┘ │
└──────────────────────────────┘ │ ┌──────────────────────┐ │
│ │ (c) SHAP │ │
Layout: '2x2' with 3 panels │ └──────────────────────┘ │
(a=top-left, b=top-right, c=btm-left) └──────────────────────────────┘
Layout: '3x1' - DON'T USE unless
panels are very wide
File naming: Fig_3_a_ROC.png → Figure 3, Panel a
Fig_3_b_SHAP.png → Figure 3, Panel b
Fig_3_c_scatter.png → Figure 3, Panel c
Panel Count to Layout Mapping:
| Panels | Preferred Layout | Panel Positions |
|---|---|---|
| 2 | 1x2 | a=left, b=right |
| 3 | 2x2 | a=top-left, b=top-right, c=bottom-left |
| 4 | 2x2 | a=top-left, b=top-right, c=bottom-left, d=bottom-right |
| 5-6 | 2x3 | Fill left-to-right, top-to-bottom |
Multi-Figure Layouts
Use these standard layouts for placing subfigure boxes (e.g., Fig 3a, 3b, 3c) on the same slide:
python1from pptx import Presentation 2from pptx.util import Inches, Pt 3from pptx.enum.text import PP_ALIGN 4from pathlib import Path 5 6# Slide dimensions 7SLIDE_WIDTH = 7.09 8SLIDE_HEIGHT = 8.47 9MARGIN = 0.5 10GAP = 0.15 11 12# Usable area 13USABLE_WIDTH = SLIDE_WIDTH - 2 * MARGIN # 6.09" 14USABLE_HEIGHT = SLIDE_HEIGHT - 2 * MARGIN # 7.47" 15 16def get_layout_positions(layout, title_height=0.6): 17 """ 18 Get positions and sizes for standard multi-figure layouts. 19 20 Args: 21 layout: '1x1', '1x2', '2x1', '2x2', '3x1', '1x3', '2x3', '3x2' 22 title_height: Height reserved for slide title (inches) 23 24 Returns: 25 List of (left, top, width, height) tuples for each panel position 26 """ 27 top_start = MARGIN + title_height 28 available_height = USABLE_HEIGHT - title_height 29 30 layouts = { 31 # Single figure (full width) 32 '1x1': [ 33 (MARGIN, top_start, USABLE_WIDTH, available_height) 34 ], 35 # 2 figures side-by-side (1 row, 2 cols) 36 '1x2': [ 37 (MARGIN, top_start, (USABLE_WIDTH - GAP) / 2, available_height), 38 (MARGIN + (USABLE_WIDTH + GAP) / 2, top_start, (USABLE_WIDTH - GAP) / 2, available_height), 39 ], 40 # 2 figures stacked (2 rows, 1 col) 41 '2x1': [ 42 (MARGIN, top_start, USABLE_WIDTH, (available_height - GAP) / 2), 43 (MARGIN, top_start + (available_height + GAP) / 2, USABLE_WIDTH, (available_height - GAP) / 2), 44 ], 45 # 2x2 grid (4 panels) 46 '2x2': [ 47 (MARGIN, top_start, (USABLE_WIDTH - GAP) / 2, (available_height - GAP) / 2), 48 (MARGIN + (USABLE_WIDTH + GAP) / 2, top_start, (USABLE_WIDTH - GAP) / 2, (available_height - GAP) / 2), 49 (MARGIN, top_start + (available_height + GAP) / 2, (USABLE_WIDTH - GAP) / 2, (available_height - GAP) / 2), 50 (MARGIN + (USABLE_WIDTH + GAP) / 2, top_start + (available_height + GAP) / 2, (USABLE_WIDTH - GAP) / 2, (available_height - GAP) / 2), 51 ], 52 # 3 figures stacked vertically (3 rows, 1 col) 53 '3x1': [ 54 (MARGIN, top_start, USABLE_WIDTH, (available_height - 2 * GAP) / 3), 55 (MARGIN, top_start + (available_height + GAP) / 3, USABLE_WIDTH, (available_height - 2 * GAP) / 3), 56 (MARGIN, top_start + 2 * (available_height + GAP) / 3, USABLE_WIDTH, (available_height - 2 * GAP) / 3), 57 ], 58 # 3 figures side-by-side (1 row, 3 cols) 59 '1x3': [ 60 (MARGIN, top_start, (USABLE_WIDTH - 2 * GAP) / 3, available_height), 61 (MARGIN + (USABLE_WIDTH + GAP) / 3, top_start, (USABLE_WIDTH - 2 * GAP) / 3, available_height), 62 (MARGIN + 2 * (USABLE_WIDTH + GAP) / 3, top_start, (USABLE_WIDTH - 2 * GAP) / 3, available_height), 63 ], 64 # 2 rows x 3 cols (6 panels) 65 '2x3': [ 66 (MARGIN, top_start, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 67 (MARGIN + (USABLE_WIDTH + GAP) / 3, top_start, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 68 (MARGIN + 2 * (USABLE_WIDTH + GAP) / 3, top_start, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 69 (MARGIN, top_start + (available_height + GAP) / 2, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 70 (MARGIN + (USABLE_WIDTH + GAP) / 3, top_start + (available_height + GAP) / 2, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 71 (MARGIN + 2 * (USABLE_WIDTH + GAP) / 3, top_start + (available_height + GAP) / 2, (USABLE_WIDTH - 2 * GAP) / 3, (available_height - GAP) / 2), 72 ], 73 } 74 return layouts.get(layout, layouts['1x1']) 75 76 77def insert_subfigure_boxes(pptx_path, slide_index, subfigures, layout, title=None): 78 """ 79 Insert subfigure boxes onto a slide, where each box can contain multiple images. 80 81 Args: 82 pptx_path: Path to the .pptx file 83 slide_index: 0-based slide index 84 subfigures: List of subfigure definitions, each is: 85 {'label': 'a', 'images': [path1, path2, ...], 'image_layout': '1x2'} 86 layout: Layout for subfigure BOXES ('1x1', '2x1', '3x1', etc.) 87 title: Optional slide title (e.g., 'Figure 3') 88 """ 89 from pptx.dml.color import RGBColor 90 from pptx.enum.shapes import MSO_SHAPE 91 92 prs = Presentation(pptx_path) 93 slide = prs.slides[slide_index] 94 95 # Add title if specified 96 title_height = 0.6 if title else 0 97 if title: 98 txBox = slide.shapes.add_textbox(Inches(MARGIN), Inches(MARGIN), 99 Inches(USABLE_WIDTH), Inches(title_height)) 100 tf = txBox.text_frame 101 p = tf.paragraphs[0] 102 p.text = title 103 p.font.size = Pt(18) 104 p.font.bold = True 105 106 # Get positions for subfigure boxes 107 box_positions = get_layout_positions(layout, title_height) 108 109 for i, subfig in enumerate(subfigures): 110 if i >= len(box_positions): 111 print(f"Warning: More subfigures than layout positions. Skipping box {subfig['label']}") 112 continue 113 114 box_left, box_top, box_width, box_height = box_positions[i] 115 label = subfig.get('label', '') 116 images = subfig.get('images', []) 117 img_layout = subfig.get('image_layout', '1x1') 118 119 # Add subfigure box border (optional - light gray rectangle) 120 box_shape = slide.shapes.add_shape( 121 MSO_SHAPE.RECTANGLE, 122 Inches(box_left), Inches(box_top), 123 Inches(box_width), Inches(box_height) 124 ) 125 box_shape.fill.background() # Transparent fill 126 box_shape.line.color.rgb = RGBColor(200, 200, 200) # Light gray border 127 box_shape.line.width = Pt(0.5) 128 129 # Add label in top-left corner of box 130 if label: 131 lbl_box = slide.shapes.add_textbox( 132 Inches(box_left + 0.05), Inches(box_top + 0.05), 133 Inches(0.3), Inches(0.25) 134 ) 135 tf = lbl_box.text_frame 136 p = tf.paragraphs[0] 137 p.text = f"({label})" 138 p.font.size = Pt(12) 139 p.font.bold = True 140 141 # Calculate image positions within the box 142 label_offset = 0.3 # Space for label 143 inner_left = box_left + 0.1 144 inner_top = box_top + label_offset 145 inner_width = box_width - 0.2 146 inner_height = box_height - label_offset - 0.1 147 inner_gap = 0.1 148 149 # Parse image layout (e.g., '1x2' = 1 row, 2 cols) 150 if 'x' in img_layout: 151 rows, cols = map(int, img_layout.split('x')) 152 else: 153 rows, cols = 1, len(images) 154 155 img_width = (inner_width - (cols - 1) * inner_gap) / cols 156 img_height = (inner_height - (rows - 1) * inner_gap) / rows 157 158 # Place images in grid within box 159 for j, img_path in enumerate(images): 160 row = j // cols 161 col = j % cols 162 img_left = inner_left + col * (img_width + inner_gap) 163 img_top = inner_top + row * (img_height + inner_gap) 164 165 slide.shapes.add_picture( 166 str(img_path), 167 Inches(img_left), Inches(img_top), 168 width=Inches(img_width), height=Inches(img_height) 169 ) 170 171 prs.save(pptx_path) 172 print(f"Inserted {len(subfigures)} subfigure boxes into slide {slide_index + 1}") 173 174 175# Example: Figure 3 with subfigure boxes a, b, c 176pptx_path = Path('Output/PowerPoint_Figures/Cardiac_RODEO_Tracked.pptx') 177 178subfigures = [ 179 { 180 'label': 'a', 181 'images': [ 182 Path('Output/ROC_Data/roc_arrhythmia.png'), 183 Path('Output/Confusion_Matrices/cm_arrhythmia.png'), 184 ], 185 'image_layout': '1x2' # 2 images side-by-side within box a 186 }, 187 { 188 'label': 'b', 189 'images': [ 190 Path('Output/ROC_Data/roc_heart_damage.png'), 191 Path('Output/Confusion_Matrices/cm_heart_damage.png'), 192 ], 193 'image_layout': '1x2' # 2 images side-by-side within box b 194 }, 195 { 196 'label': 'c', 197 'images': [ 198 Path('Output/SHAP_Data/shap_aligned_arrhythmia.png'), 199 ], 200 'image_layout': '1x1' # 1 image in box c 201 }, 202] 203 204insert_subfigure_boxes( 205 pptx_path=pptx_path, 206 slide_index=2, # Slide 3 (0-indexed) 207 subfigures=subfigures, 208 layout='3x1', # 3 subfigure boxes stacked vertically 209 title='Figure 3' 210)
Layout Reference Table
| Layout | Description | Panel Count | Best For |
|---|---|---|---|
1x1 | Single full-width | 1 | Single large figure |
1x2 | Side-by-side | 2 | Comparison (e.g., ROC vs Confusion) |
2x1 | Stacked vertical | 2 | Sequential data |
2x2 | 2×2 grid | 4 | Four related panels |
3x1 | 3 stacked vertical | 3 | Fig 3a, 3b, 3c stacked |
1x3 | 3 side-by-side | 3 | Wide comparison |
2x3 | 2×3 grid | 6 | Six panels |
Workflow for tracked figures:
- Generate and save PNG at 600 DPI
- Save Excel with data and metadata
- Insert into PowerPoint using
insert_figures_to_slide()with appropriate layout - Update
figure_registry.csv
If replacing an existing figure:
- Delete the old shape from the slide first
- Insert the new figure at the same position
- Preserve any existing labels/annotations
Figure Tracking System
Every figure must have a corresponding Excel file with the exact data used to generate it.
Naming Convention
Fig_X_letter_description.png # The figure image
Fig_X_letter_description.xlsx # The source data
Examples:
Fig_2_a_pipeline_overview.png/Fig_2_a_pipeline_overview.xlsxFig_2_b_ROC_Arrhythmia.png/Fig_2_b_ROC_Arrhythmia.xlsxFig_3_c_SHAP_importance.png/Fig_3_c_SHAP_importance.xlsx
Excel file contents should include:
- Raw data used for plotting
- Any computed values (means, standard deviations, etc.)
- Column headers matching axis labels
- A "Metadata" sheet with generation timestamp and source script
Folder Structure for PowerPoint Projects
Output/PowerPoint_Figures/
├── Fig_1/
│ ├── Fig_1a_pipeline_diagram.png
│ ├── Fig_1a_pipeline_diagram.xlsx (if applicable)
│ ├── Fig_1b_experimental_setup.png
│ └── Fig_1b_experimental_setup.xlsx
├── Fig_2/
│ ├── Fig_2a_organoid_formation.png
│ ├── Fig_2a_organoid_formation.xlsx
│ ├── Fig_2b_ROC_Arrhythmia.png
│ └── Fig_2b_ROC_Arrhythmia.xlsx
├── Fig_3/
│ └── ...
├── scripts_reference.txt # Lists all source scripts used
├── external_sources.txt # Notes externally generated images
└── figure_registry.csv # Master tracking file
Figure Registry (CSV format)
The figure_registry.csv file tracks all figures in the project.
Columns:
| Column | Description |
|---|---|
| Figure_ID | Figure number (e.g., "1", "2", "3") |
| Letter | Panel letter (e.g., "a", "b", "c") |
| Description | Brief description of the figure |
| PNG_Path | Relative path to PNG file |
| Excel_Path | Relative path to Excel data file (or "N/A") |
| Source_Script | Script that generated the figure (or "N/A") |
| External | "TRUE" if externally generated, "FALSE" otherwise |
| Notes | Additional notes (source software, manual edits, etc.) |
Example figure_registry.csv:
csv1Figure_ID,Letter,Description,PNG_Path,Excel_Path,Source_Script,External,Notes 21,a,Pipeline diagram,Fig_1/Fig_1_a_pipeline_diagram.png,N/A,N/A,TRUE,Created in BioRender 31,b,Experimental setup,Fig_1/Fig_1_b_experimental_setup.png,Fig_1/Fig_1_b_experimental_setup.xlsx,generate_setup_fig.py,FALSE, 42,a,ROC Arrhythmia,Fig_2/Fig_2_a_ROC_Arrhythmia.png,Fig_2/Fig_2_a_ROC_Arrhythmia.xlsx,plot_roc_curves.py,FALSE, 52,b,ROC Heart Damage,Fig_2/Fig_2_b_ROC_HeartDamage.png,Fig_2/Fig_2_b_ROC_HeartDamage.xlsx,plot_roc_curves.py,FALSE, 63,a,SHAP importance,Fig_3/Fig_3_a_SHAP_importance.png,Fig_3/Fig_3_a_SHAP_importance.xlsx,shap_analysis.py,FALSE,Updated 2026-01-15
Consistency Rules
CRITICAL: If one figure changes style, ALL figures must be regenerated.
-
Font Consistency
- Always use
import figure_configas the first line - Helvetica is mandatory for all text
- Standard sizes: title=14pt bold, axis labels=12pt, tick labels=10pt
- Always use
-
Color Palette Consistency
- Use the defined color palettes from this skill
- Document any custom colors in
figure_registry.csvNotes column
-
DPI Requirement
- All figures must be saved at 600 DPI
python1plt.savefig('Fig_2_a_ROC.png', dpi=600, bbox_inches='tight') -
Error Bars / Standard Deviation (MANDATORY)
- Always include error bars on bar plots, grouped bar charts, and box-and-whisker plots
-
Shared Axes for Multi-Panel Figures (MANDATORY)
- When multiple panels have the same axis range, share that axis
- Use
sharey=Trueorsharex=Trueinplt.subplots() - Only show axis labels on the leftmost (Y) or bottom (X) panel
- This reduces redundancy and makes comparisons easier
python1# CORRECT: Share Y-axis across 3 panels 2fig, axes = plt.subplots(1, 3, figsize=(8, 3), sharey=True) 3for idx, ax in enumerate(axes): 4 if idx == 0: 5 ax.set_ylabel('AUC') # Only first panel gets Y label 6 ax.set_xlabel('Accuracy') 7 8# WRONG: Repeating same Y-axis 3 times 9fig, axes = plt.subplots(1, 3, figsize=(8, 3)) 10for ax in axes: 11 ax.set_ylabel('AUC') # Redundant! -
Square Scatter Plots (MANDATORY for Accuracy vs AUC)
- Use
ax.set_box_aspect(1)to force square aspect ratio - X and Y axis ranges must be identical (e.g., both 0.3-0.9)
python1ax.set_xlim(0.3, 0.9) 2ax.set_ylim(0.3, 0.9) 3ax.set_box_aspect(1) # Force square- Use standard deviation (std) or standard error of the mean (SEM) as appropriate
- Error bars must be clearly visible with cap lines
python1# Bar plot with error bars 2ax.bar(x, means, yerr=stds, capsize=5, color='#6C92ED', edgecolor='black') 3 4# Grouped bar chart with error bars 5bars = ax.bar(x + offset, values, width, yerr=errors, capsize=3, 6 label=label, color=color, edgecolor='black') - Use
-
Regeneration Protocol When any style element changes:
- Update
figure_config.pywith new settings - Run all source scripts listed in
scripts_reference.txt - Verify all figures in
figure_registry.csvare updated - Update timestamps in Excel metadata sheets
- Update
PowerPoint Integration
Panel Labels
Figures should include letter labels (a, b, c) in boxes for multi-panel figures.
python1# Add panel label in upper-left corner 2ax.text(-0.12, 1.05, 'a', transform=ax.transAxes, 3 fontsize=16, fontweight='bold', va='top', 4 bbox=dict(boxstyle='square,pad=0.3', facecolor='white', edgecolor='black'))
Standard Panel Sizes
Based on slide dimensions: 7.09" × 8.47" (portrait), with 0.5" margins:
| Layout | Panel Width | Panel Height | Use Case |
|---|---|---|---|
Full slide (1x1) | 6.09" | 6.87" | Single large figure |
Half width (1x2) | 2.97" | 6.87" | Side-by-side panels |
Half height (2x1) | 6.09" | 3.36" | Stacked panels |
Quarter (2x2) | 2.97" | 3.36" | 4-panel grid |
Third height (3x1) | 6.09" | 2.19" | 3 stacked (Fig 3a,b,c) |
Third width (1x3) | 1.93" | 6.87" | 3 side-by-side |
Convert to matplotlib figsize (use 3x scale for 600 DPI export):
python1# Single full-slide figure 2fig, ax = plt.subplots(figsize=(6.09 * 3, 6.87 * 3)) 3 4# Half-height panel (for 2x1 or 3x1 layouts) 5fig, ax = plt.subplots(figsize=(6.09 * 3, 3.36 * 3)) 6 7# Third-height panel (for 3 stacked figures) 8fig, ax = plt.subplots(figsize=(6.09 * 3, 2.19 * 3)) 9 10# Quarter panel (for 2x2 grid) 11fig, ax = plt.subplots(figsize=(2.97 * 3, 3.36 * 3))
Alignment on Slides
- Use consistent margins (0.5" from slide edges)
- Align panel edges to PowerPoint grid
- Group related panels with consistent spacing (0.1" gap)
External Images
For images not generated by Python scripts (e.g., BioRender, microscopy, schematics):
-
Mark as "EXTERNAL" in registry
csv11,a,Pipeline diagram,Fig_1/Fig_1_a_pipeline.png,N/A,N/A,TRUE,Created in BioRender -
Note original source in
external_sources.txtFig_1a_pipeline_diagram.png - Source: BioRender - Created: 2026-01-10 - Author: Noah B. - License: Academic license, BioRender.com - Original file: pipeline_biorender_v3.png Fig_1b_microscopy.png - Source: Confocal microscope (Zeiss LSM 880) - Acquisition date: 2025-11-15 - Sample: Cardiac organoid batch #42 - Settings: 40x objective, 488nm excitation -
Still follow naming convention
- Rename external files to match
Fig_X_letter_description.pngformat - Keep originals in a separate
_originals/subfolder if needed
- Rename external files to match
Template: Creating a New PowerPoint Figure Set
python1import figure_config # FIRST LINE - registers Helvetica 2import matplotlib.pyplot as plt 3import pandas as pd 4from pathlib import Path 5from datetime import datetime 6 7# Setup paths 8output_dir = Path('Output/PowerPoint_Figures/Fig_2') 9output_dir.mkdir(parents=True, exist_ok=True) 10 11# Your data 12data = pd.DataFrame({ 13 'Model': ['Model A', 'Model B', 'Model C'], 14 'AUC': [0.85, 0.92, 0.78], 15 'AUC_std': [0.03, 0.02, 0.05] 16}) 17 18# Create figure 19fig, ax = plt.subplots(figsize=(2.42 * 3, 1.36 * 3)) # 3x scale for high-res 20 21# ... plotting code ... 22 23# Add panel label 24ax.text(-0.12, 1.05, 'a', transform=ax.transAxes, 25 fontsize=16, fontweight='bold', va='top', 26 bbox=dict(boxstyle='square,pad=0.3', facecolor='white', edgecolor='black')) 27 28plt.tight_layout() 29 30# Save figure at 600 DPI 31fig_path = output_dir / 'Fig_2_a_AUC_comparison.png' 32plt.savefig(fig_path, dpi=600, bbox_inches='tight') 33plt.close() 34 35# Save Excel with data and metadata 36excel_path = output_dir / 'Fig_2_a_AUC_comparison.xlsx' 37with pd.ExcelWriter(excel_path, engine='openpyxl') as writer: 38 data.to_excel(writer, sheet_name='Data', index=False) 39 metadata = pd.DataFrame({ 40 'Field': ['Generated', 'Script', 'Figure ID'], 41 'Value': [datetime.now().isoformat(), __file__, 'Fig_2_a'] 42 }) 43 metadata.to_excel(writer, sheet_name='Metadata', index=False) 44 45print(f"Saved: {fig_path}") 46print(f"Saved: {excel_path}")
Checklist for PowerPoint Figures
-
import figure_configis the first line - Figure saved at 600 DPI
- Naming follows
Fig_X_letter_descriptionconvention (e.g.,Fig_1_a_ROC.png) - Excel file created with matching name
- Excel includes "Metadata" sheet with timestamp and source script
- Panel label added (if multi-panel figure)
- CRITICAL: Figure inserted into PowerPoint at designated position
- Entry added to
figure_registry.csv - External sources documented in
external_sources.txt - All related figures use consistent colors/fonts/styles
- Error bars included on bar plots and box-and-whisker plots (std or SEM)