Skip to main content

💾 Saving Publication-Quality Figures

Export your visualizations for papers, presentations, and professional reports

🎯 From Screen to Publication

Creating a beautiful plot is only half the battle - you need to save it in the right format, at the right resolution, with the right settings for your intended use. Whether it's for a scientific journal, a web article, or a high-stakes presentation, the way you export your figures can make or break their impact!

Think of this as the final polish on a masterpiece. The wrong file format or resolution can turn crisp, professional visualizations into pixelated or oversized files. Master these techniques to ensure your data stories look stunning everywhere they appear!

📁 File Format Selection Guide

graph TD A[Choose Format] --> B{Purpose?} B --> C[Publication/Print] B --> D[Web/Screen] B --> E[Further Editing] C --> F[PDF
Vector, Scalable] C --> G[EPS
Legacy Publishing] D --> H[PNG
Lossless, Transparent] D --> I[JPG
Photos, Compressed] E --> J[SVG
Editable Vector] E --> K[PDF
Illustrator Compatible] style A fill:#f9f,stroke:#333,stroke-width:2px style F fill:#dc2626,stroke:#333,stroke-width:2px,color:#fff style H fill:#2563eb,stroke:#333,stroke-width:2px,color:#fff style J fill:#7c3aed,stroke:#333,stroke-width:2px,color:#fff
📄
PDF
Vector, perfect for publications
🎨
SVG
Web-ready vector graphics
🖼️
PNG
Lossless raster, transparency
📐
EPS
Legacy vector format
📷
JPG
Compressed, no transparency

🔬 DPI & Resolution Calculator

Calculate the optimal settings for your figure export:

×

📊 Export Specifications:

Pixel Dimensions: 2400 × 1800

Estimated File Size: ~1.5 MB

Recommended Format: PNG

💾 Basic Saving

Essential commands for saving figures in different formats

import matplotlib.pyplot as plt
import numpy as np

# Create a sample figure
fig, ax = plt.subplots(figsize=(8, 6))
x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), label='sin(x)')
ax.plot(x, np.cos(x), label='cos(x)')
ax.legend()
ax.set_title('Trigonometric Functions')

# Basic save - PNG format
plt.savefig('figure.png')

# Specify format explicitly
plt.savefig('figure.pdf', format='pdf')
plt.savefig('figure.svg', format='svg')
plt.savefig('figure.eps', format='eps')
plt.savefig('figure.jpg', format='jpg')

# Control quality and resolution
plt.savefig('high_quality.png', dpi=300)  # 300 DPI for print
plt.savefig('web_quality.png', dpi=72)    # 72 DPI for web

# JPEG with quality setting (0-100)
plt.savefig('compressed.jpg', quality=95, dpi=150)

# Save with tight bounding box (removes excess whitespace)
plt.savefig('tight.png', bbox_inches='tight')

# Save with transparent background
plt.savefig('transparent.png', transparent=True)

# Save with custom padding
plt.savefig('padded.png', bbox_inches='tight', pad_inches=0.5)

# Save specific figure (when you have multiple)
fig.savefig('specific_figure.png')

# Always good practice
plt.close()  # Free up memory

📐 Vector vs Raster

Understanding when to use vector or raster formats

Aspect Vector (PDF/SVG/EPS) Raster (PNG/JPG)
Scalability ✅ Infinite ❌ Fixed pixels
File Size ✅ Small for simple ❌ Large for high-res
Complex Images ❌ Can be huge ✅ Consistent size
Editing ✅ Fully editable ⚠️ Limited
Compatibility ⚠️ Varies ✅ Universal
# Vector format advantages
# PDF - Best for publications
plt.savefig('publication.pdf', 
           format='pdf',
           bbox_inches='tight')

# SVG - Best for web and editing
plt.savefig('web_figure.svg', 
           format='svg',
           bbox_inches='tight')

# EPS - Legacy format for older journals
plt.savefig('legacy.eps', 
           format='eps',
           bbox_inches='tight')

# Raster format advantages
# PNG - Lossless compression, transparency
plt.savefig('detailed_plot.png', 
           dpi=300,
           transparent=True,
           bbox_inches='tight')

# JPG - Smallest file size, no transparency
plt.savefig('photo_quality.jpg', 
           dpi=150,
           quality=90,
           bbox_inches='tight')

🎯 DPI & Resolution

Master the relationship between DPI, figure size, and output quality

# Understanding DPI (Dots Per Inch)
import matplotlib.pyplot as plt
import numpy as np

# Figure size in inches × DPI = pixel dimensions
fig_width = 8  # inches
fig_height = 6  # inches

# Different DPI settings for different uses
dpi_settings = {
    'screen': 72,      # Web display
    'print': 300,      # Standard print
    'poster': 150,     # Large format print
    'journal': 600,    # High-quality publication
}

# Create figure with specific size
fig, ax = plt.subplots(figsize=(fig_width, fig_height))

# Your plot here
x = np.linspace(0, 10, 1000)
ax.plot(x, np.sin(x) * np.exp(-x/10))

# Save at different resolutions
for use, dpi in dpi_settings.items():
    filename = f'figure_{use}_{dpi}dpi.png'
    fig.savefig(filename, dpi=dpi, bbox_inches='tight')
    
    # Calculate pixel dimensions
    width_px = fig_width * dpi
    height_px = fig_height * dpi
    print(f'{use}: {width_px:.0f}×{height_px:.0f} pixels')

# Journal submission requirements example
def save_for_journal(fig, journal_name='nature'):
    """Save figure according to journal specifications"""
    
    journal_specs = {
        'nature': {
            'single_column': 3.5,  # inches
            'double_column': 7.0,  # inches
            'dpi': 300,
            'format': 'pdf'
        },
        'science': {
            'single_column': 3.4,
            'double_column': 6.8,
            'dpi': 300,
            'format': 'eps'
        },
        'ieee': {
            'single_column': 3.5,
            'double_column': 7.16,
            'dpi': 600,
            'format': 'pdf'
        }
    }
    
    specs = journal_specs[journal_name]
    
    # Resize figure
    fig.set_size_inches(specs['single_column'], 
                       specs['single_column'] * 0.75)
    
    # Save with specifications
    fig.savefig(f'{journal_name}_figure.{specs["format"]}',
               format=specs['format'],
               dpi=specs['dpi'],
               bbox_inches='tight')

# Example usage
save_for_journal(fig, 'nature')

🎨 Advanced Export Options

Professional techniques for perfect exports

# Advanced savefig options
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
import numpy as np

# 1. Multi-page PDF
with PdfPages('multipage.pdf') as pdf:
    for i in range(4):
        fig, ax = plt.subplots()
        ax.plot(np.random.randn(100).cumsum())
        ax.set_title(f'Page {i+1}')
        
        pdf.savefig(fig, bbox_inches='tight')
        plt.close(fig)
    
    # Add metadata
    d = pdf.infodict()
    d['Title'] = 'Multipage Report'
    d['Author'] = 'Data Science Team'
    d['Subject'] = 'Quarterly Analysis'
    d['Keywords'] = 'Data, Analysis, Report'

# 2. Save with specific backend
from matplotlib.backends.backend_agg import FigureCanvasAgg
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 4, 9])
canvas = FigureCanvasAgg(fig)
canvas.print_png('backend_specific.png')

# 3. Save figure and data together
fig, ax = plt.subplots()
x = np.linspace(0, 10, 100)
y = np.sin(x)
ax.plot(x, y)

# Save figure
fig.savefig('figure_with_data.png')

# Save data
import pickle
with open('figure_data.pkl', 'wb') as f:
    pickle.dump({'figure': fig, 'x': x, 'y': y}, f)

# 4. Custom metadata in PNG
from PIL import Image
from PIL.PngImagePlugin import PngInfo

# Save figure first
fig.savefig('temp.png', dpi=300)

# Add metadata
img = Image.open('temp.png')
metadata = PngInfo()
metadata.add_text('Author', 'Your Name')
metadata.add_text('Title', 'Figure Title')
metadata.add_text('Description', 'Detailed description')
metadata.add_text('Copyright', '2024 Your Organization')
img.save('figure_with_metadata.png', pnginfo=metadata)

# 5. Save for dark/light mode
def save_both_themes(fig, filename_base):
    """Save figure in both light and dark themes"""
    
    # Light theme
    with plt.style.context('seaborn-v0_8-whitegrid'):
        fig.savefig(f'{filename_base}_light.png', 
                   facecolor='white', 
                   edgecolor='none')
    
    # Dark theme
    with plt.style.context('dark_background'):
        fig.patch.set_facecolor('#1a1a1a')
        fig.savefig(f'{filename_base}_dark.png', 
                   facecolor='#1a1a1a', 
                   edgecolor='none')

# 6. Batch export in multiple formats
def export_all_formats(fig, basename, dpi=300):
    """Export figure in all common formats"""
    formats = {
        'png': {'dpi': dpi, 'transparent': True},
        'pdf': {'bbox_inches': 'tight'},
        'svg': {'bbox_inches': 'tight'},
        'jpg': {'dpi': dpi//2, 'quality': 95}
    }
    
    for fmt, kwargs in formats.items():
        filename = f'{basename}.{fmt}'
        fig.savefig(filename, format=fmt, **kwargs)
        print(f'Saved: {filename}')

📏 Size & Layout Control

Precise control over figure dimensions and spacing

# Controlling figure size and layout for export
import matplotlib.pyplot as plt
import numpy as np

# 1. Set figure size for specific publication width
def set_size(width, fraction=1, subplots=(1, 1)):
    """Set figure dimensions to avoid scaling in LaTeX.
    
    Parameters:
        width: float - Document width in points
        fraction: float - Fraction of the width for figure
        subplots: tuple - Number of rows and columns
    """
    # Width of figure (in pts)
    fig_width_pt = width * fraction
    # Convert from pt to inches
    inches_per_pt = 1 / 72.27
    # Golden ratio
    golden_ratio = (5**.5 - 1) / 2
    # Figure width in inches
    fig_width_in = fig_width_pt * inches_per_pt
    # Figure height in inches
    fig_height_in = fig_width_in * golden_ratio * (subplots[0] / subplots[1])
    
    return (fig_width_in, fig_height_in)

# LaTeX column width (get from \the\columnwidth)
width_pts = 469.75  # Example for two-column article

fig_size = set_size(width_pts)
fig, ax = plt.subplots(figsize=fig_size)

# 2. Tight layout with padding control
fig, axes = plt.subplots(2, 2, figsize=(10, 8))

# Automatic spacing
fig.tight_layout(pad=3.0,       # Padding around figure
                w_pad=2.0,      # Width padding between subplots
                h_pad=2.0)      # Height padding between subplots

# 3. Manual spacing control
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
plt.subplots_adjust(left=0.1,    # Left margin
                   right=0.95,   # Right margin
                   top=0.95,     # Top margin
                   bottom=0.1,   # Bottom margin
                   wspace=0.2,   # Width spacing
                   hspace=0.25)  # Height spacing

# 4. Constrained layout (newer, better)
fig, axes = plt.subplots(2, 2, figsize=(10, 8),
                        constrained_layout=True)
# Set padding in inches
fig.set_constrained_layout_pads(w_pad=0.1, h_pad=0.1)

# 5. Remove all margins for seamless integration
fig, ax = plt.subplots(figsize=(8, 6))
ax.plot(np.random.randn(100))

# Remove all white space
ax.set_position([0, 0, 1, 1])
ax.axis('off')
fig.savefig('no_margins.png', bbox_inches='tight', pad_inches=0)

# 6. Save with exact pixel dimensions
def save_exact_size(fig, filename, width_px, height_px):
    """Save figure with exact pixel dimensions"""
    # Calculate required DPI
    fig_width_in, fig_height_in = fig.get_size_inches()
    dpi_w = width_px / fig_width_in
    dpi_h = height_px / fig_height_in
    dpi = max(dpi_w, dpi_h)
    
    # Adjust figure size to match exactly
    fig.set_size_inches(width_px/dpi, height_px/dpi)
    
    # Save with calculated DPI
    fig.savefig(filename, dpi=dpi, bbox_inches='tight', 
               pad_inches=0)
    
    print(f'Saved {filename}: {width_px}×{height_px}px at {dpi:.1f} DPI')

# Example: Save for specific pixel dimensions
save_exact_size(fig, 'exact_1920x1080.png', 1920, 1080)

⚙️ Optimization & Performance

Optimize file size and rendering performance

# Optimization techniques for different scenarios
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle
import matplotlib.patches as mpatches

# 1. Rasterization for complex vector elements
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6))

# Complex scatter plot (many points)
x = np.random.randn(10000)
y = np.random.randn(10000)

# Without rasterization (huge PDF)
ax1.scatter(x, y, alpha=0.5, s=1)
ax1.set_title('Vector (Large File)')

# With rasterization (small PDF)
ax2.scatter(x, y, alpha=0.5, s=1, rasterized=True)
ax2.set_title('Rasterized (Small File)')

# Save with mixed vector/raster
fig.savefig('optimized.pdf', dpi=150)

# 2. Simplify paths for web
from matplotlib.path import Path
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
x = np.linspace(0, 10, 10000)  # Many points
y = np.sin(x) + np.random.randn(10000) * 0.1

# Simplify the path
line, = ax.plot(x, y)
line.set_path_effects([path_effects.SimpleLineShadow(),
                       path_effects.Normal()])
line.set_simplify(True)
line.set_simplify_threshold(1.0)  # Adjust threshold

# 3. Reduce file size for web
def optimize_for_web(fig, filename):
    """Optimize figure for web display"""
    
    # Set reasonable DPI (screens are typically 72-96 DPI)
    web_dpi = 96
    
    # Use tight layout to remove whitespace
    fig.tight_layout()
    
    # Save as PNG with optimization
    fig.savefig(filename, 
               format='png',
               dpi=web_dpi,
               bbox_inches='tight',
               pad_inches=0.1,
               facecolor='white',
               edgecolor='none',
               optimize=True)  # PNG optimization
    
    # Alternative: Save as compressed JPG
    jpg_name = filename.replace('.png', '.jpg')
    fig.savefig(jpg_name,
               format='jpg',
               dpi=web_dpi,
               bbox_inches='tight',
               quality=85,  # Balance quality/size
               optimize=True)

# 4. Batch processing for consistency
def batch_process_figures(figures, prefix='fig', 
                         dpi=300, format='png'):
    """Process multiple figures with consistent settings"""
    
    for i, fig in enumerate(figures):
        # Apply consistent style
        fig.tight_layout()
        
        # Generate filename
        filename = f'{prefix}_{i+1:03d}.{format}'
        
        # Save with consistent settings
        fig.savefig(filename,
                   format=format,
                   dpi=dpi,
                   bbox_inches='tight',
                   facecolor='white',
                   edgecolor='none')
        
        print(f'Saved: {filename}')
        
        # Close to free memory
        plt.close(fig)

# 5. Memory management for large exports
def save_large_figure(create_figure_func, filename, 
                      chunk_size=1000):
    """Save large figure with memory management"""
    
    import gc
    
    # Create figure
    fig = create_figure_func()
    
    # Force garbage collection before save
    gc.collect()
    
    # Save with memory-efficient backend
    from matplotlib.backends.backend_agg import FigureCanvasAgg
    canvas = FigureCanvasAgg(fig)
    
    # Render and save
    canvas.print_png(filename)
    
    # Clean up
    plt.close(fig)
    del fig
    gc.collect()
    
    print(f'Saved large figure: {filename}')

🔄 Complete Export Workflow

graph TB A[Create Figure] --> B{Check Display} B --> C[Adjust Size/Layout] C --> D{Choose Format} D --> E[Vector
PDF/SVG] D --> F[Raster
PNG/JPG] E --> G[Set bbox_inches] F --> H[Set DPI] G --> I[Check File Size] H --> I I -->|Too Large| J[Optimize] I -->|OK| K[Save] J --> L[Rasterize Complex] J --> M[Reduce DPI] J --> N[Compress] L --> K M --> K N --> K K --> O[Verify Output] O -->|Issues| C O -->|Good| P[Done] style A fill:#f9f,stroke:#333,stroke-width:2px style P fill:#9f9,stroke:#333,stroke-width:2px style D fill:#ff9,stroke:#333,stroke-width:2px

🌍 Real-World Example: Complete Publication Pipeline

Professional workflow for preparing figures for publication:

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pathlib import Path
import json

class PublicationFigure:
    """Class for managing publication-quality figures"""
    
    def __init__(self, fig, metadata=None):
        self.fig = fig
        self.metadata = metadata or {}
        self.export_dir = Path('exports')
        self.export_dir.mkdir(exist_ok=True)
    
    def set_journal_style(self, journal='nature'):
        """Apply journal-specific styling"""
        
        journal_styles = {
            'nature': {
                'font.size': 7,
                'axes.labelsize': 7,
                'axes.titlesize': 7,
                'xtick.labelsize': 6,
                'ytick.labelsize': 6,
                'legend.fontsize': 6,
                'font.family': 'sans-serif',
                'font.sans-serif': ['Arial', 'Helvetica']
            },
            'science': {
                'font.size': 8,
                'axes.labelsize': 8,
                'axes.titlesize': 8,
                'xtick.labelsize': 7,
                'ytick.labelsize': 7,
                'legend.fontsize': 7,
                'font.family': 'sans-serif',
                'font.sans-serif': ['Helvetica', 'Arial']
            },
            'ieee': {
                'font.size': 8,
                'axes.labelsize': 8,
                'axes.titlesize': 9,
                'xtick.labelsize': 8,
                'ytick.labelsize': 8,
                'legend.fontsize': 8,
                'font.family': 'serif',
                'font.serif': ['Times', 'Times New Roman']
            }
        }
        
        if journal in journal_styles:
            for key, value in journal_styles[journal].items():
                plt.rcParams[key] = value
    
    def check_requirements(self, journal='nature'):
        """Check if figure meets journal requirements"""
        
        requirements = {
            'nature': {
                'max_width_single': 89,  # mm
                'max_width_double': 183,  # mm
                'max_height': 247,  # mm
                'min_dpi': 300,
                'formats': ['pdf', 'eps', 'tiff'],
                'color_mode': 'RGB'
            }
        }
        
        req = requirements.get(journal, {})
        width_inch, height_inch = self.fig.get_size_inches()
        width_mm = width_inch * 25.4
        height_mm = height_inch * 25.4
        
        checks = {
            'width_ok': width_mm <= req.get('max_width_double', float('inf')),
            'height_ok': height_mm <= req.get('max_height', float('inf')),
            'size_mm': (width_mm, height_mm)
        }
        
        return checks
    
    def export_all_formats(self, base_name, formats=None, dpi=300):
        """Export in multiple formats for different uses"""
        
        if formats is None:
            formats = ['pdf', 'svg', 'png', 'jpg']
        
        exports = {}
        
        for fmt in formats:
            filename = self.export_dir / f'{base_name}.{fmt}'
            
            if fmt in ['pdf', 'svg', 'eps']:
                # Vector formats
                self.fig.savefig(filename, 
                               format=fmt,
                               bbox_inches='tight')
            elif fmt == 'png':
                # PNG with transparency
                self.fig.savefig(filename,
                               format=fmt,
                               dpi=dpi,
                               bbox_inches='tight',
                               transparent=True)
            elif fmt == 'jpg':
                # JPEG for presentations
                self.fig.savefig(filename,
                               format=fmt,
                               dpi=dpi//2,  # Lower DPI for JPG
                               bbox_inches='tight',
                               quality=95)
            elif fmt == 'tiff':
                # TIFF for some journals
                self.fig.savefig(filename,
                               format=fmt,
                               dpi=dpi,
                               bbox_inches='tight',
                               compression='lzw')
            
            exports[fmt] = filename
            file_size = filename.stat().st_size / 1024  # KB
            print(f'Exported {fmt}: {filename.name} ({file_size:.1f} KB)')
        
        # Save metadata
        metadata_file = self.export_dir / f'{base_name}_metadata.json'
        with open(metadata_file, 'w') as f:
            json.dump({
                'base_name': base_name,
                'formats': list(exports.keys()),
                'dpi': dpi,
                'size_inches': self.fig.get_size_inches(),
                'metadata': self.metadata
            }, f, indent=2)
        
        return exports
    
    def create_submission_package(self, journal='nature'):
        """Create complete submission package"""
        
        base_name = self.metadata.get('figure_number', 'figure')
        
        # Check requirements
        checks = self.check_requirements(journal)
        print(f'Size check: {checks["size_mm"][0]:.1f}×{checks["size_mm"][1]:.1f} mm')
        print(f'Width OK: {checks["width_ok"]}, Height OK: {checks["height_ok"]}')
        
        # Export in required formats
        if journal == 'nature':
            formats = ['pdf', 'eps', 'png']
            dpi = 600
        elif journal == 'science':
            formats = ['pdf', 'tiff']
            dpi = 600
        else:
            formats = ['pdf', 'png']
            dpi = 300
        
        exports = self.export_all_formats(base_name, formats, dpi)
        
        # Create README
        readme_path = self.export_dir / f'{base_name}_README.txt'
        with open(readme_path, 'w') as f:
            f.write(f'Figure: {base_name}\n')
            f.write(f'Journal: {journal}\n')
            f.write(f'Formats: {", ".join(formats)}\n')
            f.write(f'DPI: {dpi}\n')
            f.write(f'Size: {checks["size_mm"][0]:.1f}×{checks["size_mm"][1]:.1f} mm\n')
            if self.metadata:
                f.write('\nMetadata:\n')
                for key, value in self.metadata.items():
                    f.write(f'  {key}: {value}\n')
        
        print(f'\nSubmission package created in: {self.export_dir}')
        return exports

# Example usage
def create_publication_figure():
    """Create a publication-ready figure"""
    
    # Generate sample data
    np.random.seed(42)
    x = np.linspace(0, 10, 100)
    y1 = np.sin(x) * np.exp(-x/10)
    y2 = np.cos(x) * np.exp(-x/10)
    
    # Create figure with specific size (Nature single column)
    width_mm = 89  # Single column width
    width_inch = width_mm / 25.4
    height_inch = width_inch * 0.75  # Aspect ratio
    
    fig, (ax1, ax2) = plt.subplots(2, 1, 
                                   figsize=(width_inch, height_inch),
                                   sharex=True)
    
    # Top panel
    ax1.plot(x, y1, 'b-', linewidth=1, label='Signal A')
    ax1.plot(x, y2, 'r--', linewidth=1, label='Signal B')
    ax1.set_ylabel('Amplitude (a.u.)')
    ax1.legend(loc='upper right', frameon=False)
    ax1.set_title('Figure 1: Experimental Results', fontweight='bold', pad=10)
    
    # Bottom panel
    ax2.fill_between(x, y1, y2, alpha=0.3, color='gray')
    ax2.set_xlabel('Time (s)')
    ax2.set_ylabel('Difference (a.u.)')
    
    # Remove top and right spines (Nature style)
    for ax in [ax1, ax2]:
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
    
    # Tight layout
    fig.tight_layout()
    
    # Create publication figure object
    metadata = {
        'figure_number': 'Fig1',
        'title': 'Experimental Results',
        'author': 'Research Team',
        'date': '2024-01-01',
        'description': 'Time-series analysis of experimental signals'
    }
    
    pub_fig = PublicationFigure(fig, metadata)
    
    # Apply journal style
    pub_fig.set_journal_style('nature')
    
    # Create submission package
    pub_fig.create_submission_package('nature')
    
    return pub_fig

# Run the complete pipeline
pub_fig = create_publication_figure()

# Additional specialized exports
fig = pub_fig.fig

# For presentations (16:9 aspect ratio)
fig_presentation = plt.figure(figsize=(16, 9))
# Copy content to presentation figure...
fig_presentation.savefig('presentation_slide.png', dpi=150)

# For social media (square format)
fig_social = plt.figure(figsize=(6, 6))
# Copy/adapt content for social...
fig_social.savefig('social_media.jpg', dpi=150, quality=90)

# For print poster (high resolution)
fig_poster = plt.figure(figsize=(24, 36))  # inches
# Scale up content for poster...
fig_poster.savefig('poster.pdf', format='pdf')

plt.show()

💡 Pro Tips for Publication-Quality Exports

⚠️ Common Export Mistakes