Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:30:10 +08:00
commit f0bd18fb4e
824 changed files with 331919 additions and 0 deletions

View File

@@ -0,0 +1,343 @@
#!/usr/bin/env python3
"""
Figure Export Utilities for Publication-Ready Scientific Figures
This module provides utilities to export matplotlib figures in publication-ready
formats with appropriate settings for various journals.
"""
import matplotlib.pyplot as plt
from pathlib import Path
from typing import List, Optional, Union
def save_publication_figure(
fig: plt.Figure,
filename: Union[str, Path],
formats: List[str] = ['pdf', 'png'],
dpi: int = 300,
transparent: bool = False,
bbox_inches: str = 'tight',
pad_inches: float = 0.1,
facecolor: str = 'white',
**kwargs
) -> List[Path]:
"""
Save a matplotlib figure in multiple formats with publication-quality settings.
Parameters
----------
fig : matplotlib.figure.Figure
The figure to save
filename : str or Path
Base filename (without extension)
formats : list of str, default ['pdf', 'png']
List of file formats to save. Options: 'pdf', 'png', 'eps', 'svg', 'tiff'
dpi : int, default 300
Resolution for raster formats (png, tiff). 300 DPI is minimum for most journals
transparent : bool, default False
If True, save with transparent background
bbox_inches : str, default 'tight'
Bounding box specification. 'tight' removes excess whitespace
pad_inches : float, default 0.1
Padding around the figure when bbox_inches='tight'
facecolor : str, default 'white'
Background color (ignored if transparent=True)
**kwargs
Additional keyword arguments passed to fig.savefig()
Returns
-------
list of Path
List of paths to saved files
Examples
--------
>>> fig, ax = plt.subplots()
>>> ax.plot([1, 2, 3], [1, 4, 9])
>>> save_publication_figure(fig, 'my_plot', formats=['pdf', 'png'], dpi=600)
['my_plot.pdf', 'my_plot.png']
"""
filename = Path(filename)
base_name = filename.stem
output_dir = filename.parent if filename.parent.exists() else Path.cwd()
saved_files = []
for fmt in formats:
output_file = output_dir / f"{base_name}.{fmt}"
# Set format-specific parameters
save_kwargs = {
'dpi': dpi,
'bbox_inches': bbox_inches,
'pad_inches': pad_inches,
'facecolor': facecolor if not transparent else 'none',
'edgecolor': 'none',
'transparent': transparent,
'format': fmt,
}
# Update with user-provided kwargs
save_kwargs.update(kwargs)
# Adjust DPI for vector formats (DPI less relevant)
if fmt in ['pdf', 'eps', 'svg']:
save_kwargs['dpi'] = min(dpi, 300) # Lower DPI for embedded rasters in vector
try:
fig.savefig(output_file, **save_kwargs)
saved_files.append(output_file)
print(f"✓ Saved: {output_file}")
except Exception as e:
print(f"✗ Failed to save {output_file}: {e}")
return saved_files
def save_for_journal(
fig: plt.Figure,
filename: Union[str, Path],
journal: str,
figure_type: str = 'combination'
) -> List[Path]:
"""
Save figure with journal-specific requirements.
Parameters
----------
fig : matplotlib.figure.Figure
The figure to save
filename : str or Path
Base filename (without extension)
journal : str
Journal name. Options: 'nature', 'science', 'cell', 'plos', 'acs', 'ieee'
figure_type : str, default 'combination'
Type of figure. Options: 'line_art', 'photo', 'combination'
Returns
-------
list of Path
List of paths to saved files
Examples
--------
>>> fig, ax = plt.subplots()
>>> ax.plot([1, 2, 3], [1, 4, 9])
>>> save_for_journal(fig, 'figure1', journal='nature', figure_type='line_art')
"""
journal = journal.lower()
# Define journal-specific requirements
journal_specs = {
'nature': {
'line_art': {'formats': ['pdf', 'eps'], 'dpi': 1000},
'photo': {'formats': ['tiff'], 'dpi': 300},
'combination': {'formats': ['pdf'], 'dpi': 600},
},
'science': {
'line_art': {'formats': ['eps', 'pdf'], 'dpi': 1000},
'photo': {'formats': ['tiff'], 'dpi': 300},
'combination': {'formats': ['eps'], 'dpi': 600},
},
'cell': {
'line_art': {'formats': ['pdf', 'eps'], 'dpi': 1000},
'photo': {'formats': ['tiff'], 'dpi': 300},
'combination': {'formats': ['pdf'], 'dpi': 600},
},
'plos': {
'line_art': {'formats': ['pdf', 'eps'], 'dpi': 600},
'photo': {'formats': ['tiff', 'png'], 'dpi': 300},
'combination': {'formats': ['tiff'], 'dpi': 300},
},
'acs': {
'line_art': {'formats': ['tiff', 'pdf'], 'dpi': 600},
'photo': {'formats': ['tiff'], 'dpi': 300},
'combination': {'formats': ['tiff'], 'dpi': 600},
},
'ieee': {
'line_art': {'formats': ['pdf', 'eps'], 'dpi': 600},
'photo': {'formats': ['tiff'], 'dpi': 300},
'combination': {'formats': ['pdf'], 'dpi': 300},
},
}
if journal not in journal_specs:
available = ', '.join(journal_specs.keys())
raise ValueError(f"Journal '{journal}' not recognized. Available: {available}")
if figure_type not in journal_specs[journal]:
available = ', '.join(journal_specs[journal].keys())
raise ValueError(f"Figure type '{figure_type}' not valid. Available: {available}")
specs = journal_specs[journal][figure_type]
print(f"Saving for {journal.upper()} ({figure_type}):")
print(f" Formats: {', '.join(specs['formats'])}")
print(f" DPI: {specs['dpi']}")
return save_publication_figure(
fig=fig,
filename=filename,
formats=specs['formats'],
dpi=specs['dpi']
)
def check_figure_size(fig: plt.Figure, journal: str = 'nature') -> dict:
"""
Check if figure dimensions are appropriate for journal requirements.
Parameters
----------
fig : matplotlib.figure.Figure
The figure to check
journal : str, default 'nature'
Journal name
Returns
-------
dict
Dictionary with figure dimensions and compliance status
Examples
--------
>>> fig = plt.figure(figsize=(3.5, 3))
>>> info = check_figure_size(fig, journal='nature')
>>> print(info)
"""
journal = journal.lower()
# Get figure dimensions in inches
width_inches, height_inches = fig.get_size_inches()
width_mm = width_inches * 25.4
height_mm = height_inches * 25.4
# Journal specifications (widths in mm)
specs = {
'nature': {'single': 89, 'double': 183, 'max_height': 247},
'science': {'single': 55, 'double': 175, 'max_height': 233},
'cell': {'single': 85, 'double': 178, 'max_height': 230},
'plos': {'single': 83, 'double': 173, 'max_height': 233},
'acs': {'single': 82.5, 'double': 178, 'max_height': 247},
}
if journal not in specs:
journal_spec = specs['nature']
print(f"Warning: Journal '{journal}' not found, using Nature specifications")
else:
journal_spec = specs[journal]
# Determine column type
column_type = None
width_ok = False
tolerance = 5 # mm tolerance
if abs(width_mm - journal_spec['single']) < tolerance:
column_type = 'single'
width_ok = True
elif abs(width_mm - journal_spec['double']) < tolerance:
column_type = 'double'
width_ok = True
height_ok = height_mm <= journal_spec['max_height']
result = {
'width_inches': width_inches,
'height_inches': height_inches,
'width_mm': width_mm,
'height_mm': height_mm,
'journal': journal,
'column_type': column_type,
'width_ok': width_ok,
'height_ok': height_ok,
'compliant': width_ok and height_ok,
'recommendations': {
'single_column_mm': journal_spec['single'],
'double_column_mm': journal_spec['double'],
'max_height_mm': journal_spec['max_height'],
}
}
# Print report
print(f"\n{'='*60}")
print(f"Figure Size Check for {journal.upper()}")
print(f"{'='*60}")
print(f"Current size: {width_mm:.1f} × {height_mm:.1f} mm")
print(f" ({width_inches:.2f} × {height_inches:.2f} inches)")
print(f"\n{journal.upper()} specifications:")
print(f" Single column: {journal_spec['single']} mm")
print(f" Double column: {journal_spec['double']} mm")
print(f" Max height: {journal_spec['max_height']} mm")
print(f"\nCompliance:")
print(f" Width: {'✓ OK' if width_ok else '✗ Non-standard'} ({column_type or 'custom'})")
print(f" Height: {'✓ OK' if height_ok else '✗ Too tall'}")
print(f" Overall: {'✓ COMPLIANT' if result['compliant'] else '✗ NEEDS ADJUSTMENT'}")
print(f"{'='*60}\n")
return result
def verify_font_embedding(pdf_path: Union[str, Path]) -> bool:
"""
Check if fonts are embedded in a PDF file.
Note: This requires PyPDF2 or a similar library to be installed.
Parameters
----------
pdf_path : str or Path
Path to PDF file
Returns
-------
bool
True if fonts are embedded, False otherwise
"""
try:
from PyPDF2 import PdfReader
except ImportError:
print("Warning: PyPDF2 not installed. Cannot verify font embedding.")
print("Install with: pip install PyPDF2")
return None
pdf_path = Path(pdf_path)
try:
reader = PdfReader(pdf_path)
# This is a simplified check; full verification is complex
print(f"PDF has {len(reader.pages)} page(s)")
print("Note: Full font embedding verification requires detailed PDF inspection.")
return True
except Exception as e:
print(f"Error reading PDF: {e}")
return False
if __name__ == "__main__":
# Example usage
import numpy as np
# Create example figure
fig, ax = plt.subplots(figsize=(3.5, 2.5))
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.set_xlabel('x')
ax.set_ylabel('y')
ax.legend()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
# Check size
check_figure_size(fig, journal='nature')
# Save in multiple formats
print("\nSaving figure...")
save_publication_figure(fig, 'example_figure', formats=['pdf', 'png'], dpi=300)
# Save with journal-specific requirements
print("\nSaving for Nature...")
save_for_journal(fig, 'example_figure_nature', journal='nature', figure_type='line_art')
plt.close(fig)

View File

@@ -0,0 +1,416 @@
#!/usr/bin/env python3
"""
Matplotlib Style Presets for Publication-Ready Scientific Figures
This module provides pre-configured matplotlib styles optimized for
different journals and use cases.
"""
import matplotlib.pyplot as plt
import matplotlib as mpl
from typing import Optional, Dict, Any
# Okabe-Ito colorblind-friendly palette
OKABE_ITO_COLORS = [
'#E69F00', # Orange
'#56B4E9', # Sky Blue
'#009E73', # Bluish Green
'#F0E442', # Yellow
'#0072B2', # Blue
'#D55E00', # Vermillion
'#CC79A7', # Reddish Purple
'#000000' # Black
]
# Paul Tol palettes
TOL_BRIGHT = ['#4477AA', '#EE6677', '#228833', '#CCBB44', '#66CCEE', '#AA3377', '#BBBBBB']
TOL_MUTED = ['#332288', '#88CCEE', '#44AA99', '#117733', '#999933', '#DDCC77', '#CC6677', '#882255', '#AA4499']
TOL_HIGH_CONTRAST = ['#004488', '#DDAA33', '#BB5566']
# Wong palette
WONG_COLORS = ['#000000', '#E69F00', '#56B4E9', '#009E73', '#F0E442', '#0072B2', '#D55E00', '#CC79A7']
def get_base_style() -> Dict[str, Any]:
"""
Get base publication-quality style settings.
Returns
-------
dict
Dictionary of matplotlib rcParams
"""
return {
# Figure
'figure.dpi': 100, # Display DPI (changed on save)
'figure.facecolor': 'white',
'figure.autolayout': False,
'figure.constrained_layout.use': True,
# Font
'font.size': 8,
'font.family': 'sans-serif',
'font.sans-serif': ['Arial', 'Helvetica', 'DejaVu Sans'],
# Axes
'axes.linewidth': 0.5,
'axes.labelsize': 9,
'axes.titlesize': 9,
'axes.labelweight': 'normal',
'axes.spines.top': False,
'axes.spines.right': False,
'axes.spines.left': True,
'axes.spines.bottom': True,
'axes.edgecolor': 'black',
'axes.labelcolor': 'black',
'axes.axisbelow': True,
'axes.prop_cycle': mpl.cycler(color=OKABE_ITO_COLORS),
# Grid
'axes.grid': False,
# Ticks
'xtick.major.size': 3,
'xtick.minor.size': 2,
'xtick.major.width': 0.5,
'xtick.minor.width': 0.5,
'xtick.labelsize': 7,
'xtick.direction': 'out',
'ytick.major.size': 3,
'ytick.minor.size': 2,
'ytick.major.width': 0.5,
'ytick.minor.width': 0.5,
'ytick.labelsize': 7,
'ytick.direction': 'out',
# Lines
'lines.linewidth': 1.5,
'lines.markersize': 4,
'lines.markeredgewidth': 0.5,
# Legend
'legend.fontsize': 7,
'legend.frameon': False,
'legend.loc': 'best',
# Savefig
'savefig.dpi': 300,
'savefig.format': 'pdf',
'savefig.bbox': 'tight',
'savefig.pad_inches': 0.05,
'savefig.transparent': False,
'savefig.facecolor': 'white',
# Image
'image.cmap': 'viridis',
'image.aspect': 'auto',
}
def apply_publication_style(style_name: str = 'default') -> None:
"""
Apply a pre-configured publication style.
Parameters
----------
style_name : str, default 'default'
Name of the style to apply. Options:
- 'default': General publication style
- 'nature': Nature journal style
- 'science': Science journal style
- 'cell': Cell Press style
- 'minimal': Minimal clean style
- 'presentation': Larger fonts for presentations
Examples
--------
>>> apply_publication_style('nature')
>>> fig, ax = plt.subplots()
>>> ax.plot([1, 2, 3], [1, 4, 9])
"""
base_style = get_base_style()
# Style-specific modifications
if style_name == 'nature':
base_style.update({
'font.size': 7,
'axes.labelsize': 8,
'axes.titlesize': 8,
'xtick.labelsize': 6,
'ytick.labelsize': 6,
'legend.fontsize': 6,
'savefig.dpi': 600,
})
elif style_name == 'science':
base_style.update({
'font.size': 7,
'axes.labelsize': 8,
'xtick.labelsize': 6,
'ytick.labelsize': 6,
'legend.fontsize': 6,
'savefig.dpi': 600,
})
elif style_name == 'cell':
base_style.update({
'font.size': 8,
'axes.labelsize': 9,
'xtick.labelsize': 7,
'ytick.labelsize': 7,
'legend.fontsize': 7,
'savefig.dpi': 600,
})
elif style_name == 'minimal':
base_style.update({
'axes.linewidth': 0.8,
'xtick.major.width': 0.8,
'ytick.major.width': 0.8,
'lines.linewidth': 2,
})
elif style_name == 'presentation':
base_style.update({
'font.size': 14,
'axes.labelsize': 16,
'axes.titlesize': 18,
'xtick.labelsize': 12,
'ytick.labelsize': 12,
'legend.fontsize': 12,
'axes.linewidth': 1.5,
'lines.linewidth': 2.5,
'lines.markersize': 8,
})
elif style_name != 'default':
print(f"Warning: Style '{style_name}' not recognized. Using 'default'.")
# Apply the style
plt.rcParams.update(base_style)
print(f"✓ Applied '{style_name}' publication style")
def set_color_palette(palette_name: str = 'okabe_ito') -> None:
"""
Set a colorblind-friendly color palette.
Parameters
----------
palette_name : str, default 'okabe_ito'
Name of the palette. Options:
- 'okabe_ito': Okabe-Ito palette (8 colors)
- 'wong': Wong palette (8 colors)
- 'tol_bright': Paul Tol bright palette (7 colors)
- 'tol_muted': Paul Tol muted palette (9 colors)
- 'tol_high_contrast': Paul Tol high contrast (3 colors)
Examples
--------
>>> set_color_palette('tol_muted')
>>> fig, ax = plt.subplots()
>>> for i in range(5):
... ax.plot([1, 2, 3], [i, i+1, i+2])
"""
palettes = {
'okabe_ito': OKABE_ITO_COLORS,
'wong': WONG_COLORS,
'tol_bright': TOL_BRIGHT,
'tol_muted': TOL_MUTED,
'tol_high_contrast': TOL_HIGH_CONTRAST,
}
if palette_name not in palettes:
available = ', '.join(palettes.keys())
print(f"Warning: Palette '{palette_name}' not found. Available: {available}")
palette_name = 'okabe_ito'
colors = palettes[palette_name]
plt.rcParams['axes.prop_cycle'] = plt.cycler(color=colors)
print(f"✓ Applied '{palette_name}' color palette ({len(colors)} colors)")
def configure_for_journal(journal: str, figure_width: str = 'single') -> None:
"""
Configure matplotlib for a specific journal.
Parameters
----------
journal : str
Journal name: 'nature', 'science', 'cell', 'plos', 'acs', 'ieee'
figure_width : str, default 'single'
Figure width: 'single' or 'double' column
Examples
--------
>>> configure_for_journal('nature', figure_width='single')
>>> fig, ax = plt.subplots() # Will have correct size for Nature
"""
journal = journal.lower()
# Journal specifications
journal_configs = {
'nature': {
'single_width': 89, # mm
'double_width': 183,
'style': 'nature',
},
'science': {
'single_width': 55,
'double_width': 175,
'style': 'science',
},
'cell': {
'single_width': 85,
'double_width': 178,
'style': 'cell',
},
'plos': {
'single_width': 83,
'double_width': 173,
'style': 'default',
},
'acs': {
'single_width': 82.5,
'double_width': 178,
'style': 'default',
},
'ieee': {
'single_width': 89,
'double_width': 182,
'style': 'default',
},
}
if journal not in journal_configs:
available = ', '.join(journal_configs.keys())
raise ValueError(f"Journal '{journal}' not recognized. Available: {available}")
config = journal_configs[journal]
# Apply style
apply_publication_style(config['style'])
# Set default figure size
width_mm = config['single_width'] if figure_width == 'single' else config['double_width']
width_inches = width_mm / 25.4
plt.rcParams['figure.figsize'] = (width_inches, width_inches * 0.75) # 4:3 aspect ratio
print(f"✓ Configured for {journal.upper()} ({figure_width} column: {width_mm} mm)")
def create_style_template(output_file: str = 'publication.mplstyle') -> None:
"""
Create a matplotlib style file that can be used with plt.style.use().
Parameters
----------
output_file : str, default 'publication.mplstyle'
Output filename for the style file
Examples
--------
>>> create_style_template('my_style.mplstyle')
>>> plt.style.use('my_style.mplstyle')
"""
style = get_base_style()
with open(output_file, 'w') as f:
f.write("# Publication-quality matplotlib style\n")
f.write("# Usage: plt.style.use('publication.mplstyle')\n\n")
for key, value in style.items():
if isinstance(value, mpl.cycler):
# Handle cycler specially
colors = [c['color'] for c in value]
f.write(f"axes.prop_cycle : cycler('color', {colors})\n")
else:
f.write(f"{key} : {value}\n")
print(f"✓ Created style template: {output_file}")
print(f" Use with: plt.style.use('{output_file}')")
def show_color_palettes() -> None:
"""
Display available color palettes for visual inspection.
"""
palettes = {
'Okabe-Ito': OKABE_ITO_COLORS,
'Wong': WONG_COLORS,
'Tol Bright': TOL_BRIGHT,
'Tol Muted': TOL_MUTED,
'Tol High Contrast': TOL_HIGH_CONTRAST,
}
fig, axes = plt.subplots(len(palettes), 1, figsize=(8, len(palettes) * 0.5))
for ax, (name, colors) in zip(axes, palettes.items()):
ax.set_xlim(0, len(colors))
ax.set_ylim(0, 1)
ax.set_yticks([])
ax.set_xticks([])
ax.set_ylabel(name, fontsize=10)
for i, color in enumerate(colors):
ax.add_patch(plt.Rectangle((i, 0), 1, 1, facecolor=color, edgecolor='black', linewidth=0.5))
# Add hex code
ax.text(i + 0.5, 0.5, color, ha='center', va='center',
fontsize=7, color='white' if i >= len(colors) - 1 else 'black')
fig.suptitle('Colorblind-Friendly Palettes', fontsize=12, fontweight='bold')
plt.tight_layout()
plt.show()
def reset_to_default() -> None:
"""
Reset matplotlib to default settings.
"""
mpl.rcdefaults()
print("✓ Reset to matplotlib defaults")
if __name__ == "__main__":
print("Matplotlib Style Presets for Scientific Figures")
print("=" * 50)
# Show available styles
print("\nAvailable publication styles:")
print(" - default")
print(" - nature")
print(" - science")
print(" - cell")
print(" - minimal")
print(" - presentation")
print("\nAvailable color palettes:")
print(" - okabe_ito (recommended)")
print(" - wong")
print(" - tol_bright")
print(" - tol_muted")
print(" - tol_high_contrast")
print("\nExample usage:")
print(" from style_presets import apply_publication_style, set_color_palette")
print(" apply_publication_style('nature')")
print(" set_color_palette('okabe_ito')")
# Create example figure
print("\nGenerating example figure with 'default' style...")
apply_publication_style('default')
fig, ax = plt.subplots(figsize=(3.5, 2.5))
for i in range(5):
ax.plot([1, 2, 3, 4], [i, i+1, i+0.5, i+2], marker='o', label=f'Series {i+1}')
ax.set_xlabel('Time (hours)')
ax.set_ylabel('Response (AU)')
ax.legend()
fig.suptitle('Example with Publication Style')
plt.tight_layout()
plt.show()
# Show color palettes
print("\nDisplaying color palettes...")
show_color_palettes()