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