Skip to main content

🎨 Customizing Plots

Transform basic charts into stunning, publication-ready visualizations

✨ The Art of Visual Polish

Imagine you're a designer with unlimited creative control over every pixel of your visualization. Colors that tell stories, fonts that convey authority, annotations that guide the eye - these details transform good charts into great ones!

In professional data science, the difference between a plot that gets ignored and one that drives decisions often comes down to customization. A well-styled chart isn't just prettier - it's clearer, more persuasive, and more memorable. Let's master the tools that turn data into visual masterpieces!

🎨 Elements of Plot Customization

graph TB A[Plot Customization] --> B[Colors & Styles] A --> C[Typography] A --> D[Annotations] A --> E[Legends & Labels] A --> F[Axes & Grids] A --> G[Themes & Styles] B --> B1[Line Colors] B --> B2[Fill Colors] B --> B3[Colormaps] B --> B4[Transparency] C --> C1[Font Family] C --> C2[Font Size] C --> C3[Font Weight] C --> C4[Text Rotation] D --> D1[Text Annotations] D --> D2[Arrows] D --> D3[Shapes] D --> D4[Highlights] style A fill:#f9f,stroke:#333,stroke-width:2px style B fill:#ff9,stroke:#333,stroke-width:2px style C fill:#9ff,stroke:#333,stroke-width:2px style D fill:#f99,stroke:#333,stroke-width:2px

🎮 Interactive Customization Playground

Experiment with different customization options in real-time:

🎨 Colors

70%

📐 Lines & Markers

2

🔤 Typography

🎨 Color Mastery

Use colors effectively to enhance understanding and appeal

# Basic color options
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 100)
y1 = np.sin(x)
y2 = np.cos(x)

# Named colors
plt.plot(x, y1, color='crimson', label='Sin')
plt.plot(x, y2, color='forestgreen', label='Cos')

# Hex colors
plt.plot(x, y1, color='#FF5733', label='Custom')

# RGB tuples (values 0-1)
plt.plot(x, y2, color=(0.1, 0.2, 0.5), label='RGB')

# With transparency (alpha)
plt.plot(x, y1, color='blue', alpha=0.5)
plt.fill_between(x, 0, y1, color='blue', alpha=0.3)

# Colormaps for multiple lines
colors = plt.cm.viridis(np.linspace(0, 1, 5))
for i, color in enumerate(colors):
    plt.plot(x, np.sin(x + i), color=color)

# Custom color palette
from matplotlib.colors import ListedColormap
custom_colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A']
cmap = ListedColormap(custom_colors)

# Categorical colors
from matplotlib.colors import Normalize
from matplotlib.cm import ScalarMappable

fig, ax = plt.subplots()
categories = np.random.choice([0, 1, 2, 3], 100)
colors = [custom_colors[cat] for cat in categories]
scatter = ax.scatter(np.random.randn(100), 
                    np.random.randn(100), 
                    c=colors, s=100, alpha=0.6)

plt.show()

🔤 Typography & Text

Control every aspect of text appearance

# Font customization
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm

fig, ax = plt.subplots(figsize=(10, 6))

# Title with custom font
ax.set_title('Custom Typography Example', 
            fontsize=20,
            fontweight='bold',
            fontfamily='serif',
            color='#2E86AB',
            pad=20)  # Padding from axes

# Axis labels with different styles
ax.set_xlabel('X Axis Label', 
             fontsize=14,
             fontstyle='italic',
             fontweight='normal')

ax.set_ylabel('Y Axis Label',
             fontsize=14,
             rotation=90,
             labelpad=10)

# Custom tick labels
ax.tick_params(axis='x', 
              labelsize=12,
              labelcolor='gray',
              labelrotation=45)

ax.tick_params(axis='y',
              labelsize=12,
              labelcolor='gray')

# Text annotations with boxes
ax.text(0.5, 0.5, 'Centered Text',
       transform=ax.transAxes,  # Use axes coordinates
       fontsize=16,
       ha='center', va='center',
       bbox=dict(boxstyle='round,pad=0.5',
                facecolor='yellow',
                edgecolor='black',
                alpha=0.8))

# Mathematical expressions (LaTeX)
ax.text(0.1, 0.9, r'$\sum_{i=1}^{n} x_i = \frac{1}{2}\alpha$',
       transform=ax.transAxes,
       fontsize=14)

# Using system fonts
available_fonts = [f.name for f in fm.fontManager.ttflist]
# print(available_fonts)  # List all available fonts

# Font properties
from matplotlib import font_manager
prop = font_manager.FontProperties(fname='/path/to/font.ttf')
ax.text(0.5, 0.2, 'Custom Font File', fontproperties=prop)

# Global font settings
plt.rcParams['font.family'] = 'sans-serif'
plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
plt.rcParams['font.size'] = 12

plt.show()

📍 Annotations & Arrows

Guide attention with annotations, arrows, and callouts

# Advanced annotations
import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots(figsize=(10, 6))

x = np.linspace(0, 10, 100)
y = np.sin(x) * np.exp(-x/10)
ax.plot(x, y, 'b-', linewidth=2)

# Simple annotation with arrow
max_idx = np.argmax(y)
ax.annotate('Peak Value',
           xy=(x[max_idx], y[max_idx]),  # Point to annotate
           xytext=(x[max_idx]+2, y[max_idx]+0.2),  # Text location
           arrowprops=dict(arrowstyle='->',
                          connectionstyle='arc3,rad=0.3',
                          color='red',
                          lw=2))

# Fancy arrow styles
arrow_styles = ['->', '-[', '|-|', '-|>', '<->', 'fancy']
for i, style in enumerate(arrow_styles[:4]):
    ax.annotate(f'Style: {style}',
               xy=(i*2, 0.5), xytext=(i*2, 0.7),
               arrowprops=dict(arrowstyle=style))

# Annotation with custom bbox
ax.annotate('Important Region',
           xy=(5, 0), xytext=(6, -0.3),
           bbox=dict(boxstyle="round,pad=0.3",
                    facecolor='yellow',
                    edgecolor='red',
                    linewidth=2),
           arrowprops=dict(arrowstyle='->',
                          connectionstyle='angle3,angleA=90,angleB=0',
                          color='red'))

# Highlighting regions
ax.axvspan(3, 5, alpha=0.3, color='green', 
          label='Region of Interest')
ax.axhline(y=0, color='gray', linestyle='--', alpha=0.5)
ax.axvline(x=7, color='gray', linestyle='--', alpha=0.5)

# Adding shapes
from matplotlib.patches import Circle, Rectangle, Ellipse, Polygon

# Circle
circle = Circle((2, 0.5), 0.2, color='blue', alpha=0.3)
ax.add_patch(circle)

# Rectangle
rect = Rectangle((6, -0.2), 1, 0.4, 
                facecolor='red', alpha=0.3,
                edgecolor='black', linewidth=2)
ax.add_patch(rect)

# Ellipse
ellipse = Ellipse((8, 0.3), width=1, height=0.3,
                  angle=45, facecolor='green', alpha=0.3)
ax.add_patch(ellipse)

# Custom polygon
polygon = Polygon([[1, 0], [1.5, 0.3], [2, 0], [1.5, -0.2]],
                 facecolor='orange', alpha=0.5)
ax.add_patch(polygon)

# Connection patches between points
from matplotlib.patches import ConnectionPatch
con = ConnectionPatch((1, 0.5), (3, 0.3), "data", "data",
                     arrowstyle="-|>", shrinkA=5, shrinkB=5,
                     mutation_scale=20, fc="red", ec="red", lw=2)
ax.add_artist(con)

plt.legend()
plt.show()

📊 Line & Marker Styles

Comprehensive guide to line styles and markers

# Line and marker combinations
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0, 10, 20)

# Line styles
linestyles = ['-', '--', '-.', ':', 'None']
markers = ['o', 's', '^', 'd', 'v', '<', '>', 
          'p', '*', 'h', 'H', '+', 'x', 'D']

fig, axes = plt.subplots(2, 1, figsize=(12, 8))

# Line style showcase
ax1 = axes[0]
for i, ls in enumerate(linestyles[:4]):
    y = np.sin(x + i*0.5)
    ax1.plot(x, y, linestyle=ls, linewidth=2, 
            label=f'Linestyle: {ls}')
ax1.legend()
ax1.set_title('Line Styles')

# Marker showcase
ax2 = axes[1]
for i, marker in enumerate(markers[:5]):
    y = np.cos(x + i*0.5) * 0.8
    ax2.plot(x[::2], y[::2], marker=marker, 
            markersize=10, linestyle='-', 
            linewidth=1, alpha=0.7,
            markerfacecolor='white',
            markeredgecolor='auto',
            markeredgewidth=2,
            label=f'Marker: {marker}')
ax2.legend()
ax2.set_title('Marker Styles')

# Advanced marker customization
fig2, ax = plt.subplots(figsize=(10, 6))

# Custom marker properties
ax.plot(x, np.sin(x), 'o-',
       markersize=12,
       markerfacecolor='yellow',
       markeredgecolor='red',
       markeredgewidth=2,
       markevery=3,  # Show marker every 3rd point
       label='Custom markers')

# Different markers for different data points
y = np.cos(x)
colors = plt.cm.rainbow(np.linspace(0, 1, len(x)))
for i, (xi, yi, c) in enumerate(zip(x, y, colors)):
    marker = markers[i % len(markers)]
    ax.plot(xi, yi, marker=marker, markersize=10, 
           color=c, markeredgecolor='black')

plt.tight_layout()
plt.show()

🎭 Themes & Styles

Apply consistent themes across all your plots

# Using and creating custom styles
import matplotlib.pyplot as plt
import matplotlib as mpl
import numpy as np

# Built-in styles
print(plt.style.available)  # List all available styles

# Apply a style
plt.style.use('seaborn-v0_8-darkgrid')
# or temporarily
with plt.style.context('ggplot'):
    plt.plot([1, 2, 3], [1, 4, 9])
    plt.show()

# Create custom style
custom_style = {
    # Figure
    'figure.figsize': (10, 6),
    'figure.facecolor': 'white',
    'figure.edgecolor': 'none',
    'figure.dpi': 100,
    
    # Axes
    'axes.facecolor': '#f8f9fa',
    'axes.edgecolor': '#dee2e6',
    'axes.linewidth': 1,
    'axes.grid': True,
    'axes.titlesize': 16,
    'axes.titleweight': 'bold',
    'axes.labelsize': 12,
    'axes.labelweight': 'normal',
    'axes.spines.top': False,
    'axes.spines.right': False,
    
    # Grid
    'grid.color': '#e9ecef',
    'grid.linestyle': '-',
    'grid.linewidth': 0.8,
    'grid.alpha': 0.5,
    
    # Lines
    'lines.linewidth': 2,
    'lines.markersize': 8,
    
    # Colors (color cycle)
    'axes.prop_cycle': mpl.cycler(color=[
        '#2E86AB', '#A23B72', '#F18F01', 
        '#C73E1D', '#6C464E', '#395B50'
    ]),
    
    # Font
    'font.family': 'sans-serif',
    'font.sans-serif': ['Arial', 'DejaVu Sans'],
    'font.size': 11,
    
    # Legend
    'legend.frameon': True,
    'legend.framealpha': 0.9,
    'legend.facecolor': 'white',
    'legend.edgecolor': '#dee2e6',
    'legend.fontsize': 10,
    
    # Ticks
    'xtick.labelsize': 10,
    'ytick.labelsize': 10,
    'xtick.color': '#495057',
    'ytick.color': '#495057',
}

# Apply custom style
for key, value in custom_style.items():
    plt.rcParams[key] = value

# Reset to defaults
mpl.rcdefaults()

# Save style to file
import json
# Save as JSON
with open('my_style.json', 'w') as f:
    json.dump(custom_style, f)

# Create a .mplstyle file
with open('my_style.mplstyle', 'w') as f:
    for key, value in custom_style.items():
        f.write(f'{key}: {value}\n')

# Use custom style file
plt.style.use('my_style.mplstyle')

# Context manager for temporary style changes
with plt.rc_context(custom_style):
    fig, ax = plt.subplots()
    ax.plot([1, 2, 3], [1, 4, 9])
    plt.show()

# Dark theme example
dark_theme = {
    'figure.facecolor': '#1a1a1a',
    'axes.facecolor': '#2b2b2b',
    'axes.edgecolor': '#555',
    'axes.labelcolor': '#ccc',
    'text.color': '#ccc',
    'xtick.color': '#ccc',
    'ytick.color': '#ccc',
    'grid.color': '#444',
    'legend.facecolor': '#2b2b2b',
    'legend.edgecolor': '#555',
}

🌈 Colormaps

Choose the perfect colormap for your data

# Colormap selection and customization
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import numpy as np

# Generate sample data
x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig, axes = plt.subplots(2, 3, figsize=(12, 8))

# Different colormap categories
cmaps = {
    'Sequential': 'viridis',
    'Diverging': 'RdBu_r',
    'Qualitative': 'Set3',
    'Cyclic': 'twilight',
    'Sequential2': 'plasma',
    'Perceptual': 'cividis'
}

for ax, (name, cmap) in zip(axes.flat, cmaps.items()):
    im = ax.imshow(Z, cmap=cmap, extent=[-3, 3, -3, 3])
    ax.set_title(f'{name}: {cmap}')
    plt.colorbar(im, ax=ax, fraction=0.046)

# Custom colormap
from matplotlib.colors import LinearSegmentedColormap

# Define colors
colors = ['#FF6B6B', '#FFE66D', '#4ECDC4', '#45B7D1']
n_bins = 100
cmap = LinearSegmentedColormap.from_list('custom', colors, N=n_bins)

# Discrete colormap
discrete_cmap = LinearSegmentedColormap.from_list(
    'discrete', colors, N=len(colors))

# Colormap with specific levels
levels = [-1, -0.5, 0, 0.5, 1]
from matplotlib.colors import BoundaryNorm
norm = BoundaryNorm(levels, ncolors=256, extend='both')

# Colormap for categorical data
from matplotlib.colors import ListedColormap
cat_colors = ['#E74C3C', '#3498DB', '#2ECC71', '#F39C12']
cat_cmap = ListedColormap(cat_colors)

# Accessing specific colors from colormap
viridis = cm.get_cmap('viridis')
colors_from_map = viridis(np.linspace(0, 1, 10))

# Reversed colormap
reversed_cmap = cm.get_cmap('viridis_r')  # Add _r for reversed

# Truncated colormap
def truncate_colormap(cmap, minval=0.0, maxval=1.0, n=100):
    new_cmap = LinearSegmentedColormap.from_list(
        f'trunc({cmap.name},{minval:.2f},{maxval:.2f})',
        cmap(np.linspace(minval, maxval, n)))
    return new_cmap

trunc_viridis = truncate_colormap(viridis, 0.2, 0.8)

plt.tight_layout()
plt.show()

🌍 Real-World Example: Publication-Ready Scientific Figure

Create a fully customized, publication-quality figure combining all techniques:

import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np
from matplotlib.patches import Rectangle, FancyBboxPatch
from matplotlib.collections import PatchCollection
import matplotlib.patches as mpatches

# Set up the figure with golden ratio
golden_ratio = (1 + np.sqrt(5)) / 2
fig_width = 12
fig_height = fig_width / golden_ratio

# Create figure with custom style
plt.style.use('seaborn-v0_8-whitegrid')
fig = plt.figure(figsize=(fig_width, fig_height))

# Custom color palette (colorblind-friendly)
colors = {
    'primary': '#0173B2',
    'secondary': '#DE8F05',
    'tertiary': '#029E73',
    'quaternary': '#CC78BC',
    'highlight': '#ECE133',
    'dark': '#2E3440',
    'light': '#ECEFF4'
}

# Create GridSpec for complex layout
gs = gridspec.GridSpec(3, 3, 
                      width_ratios=[1.5, 1, 1],
                      height_ratios=[1, 1, 0.5],
                      wspace=0.3, hspace=0.4)

# Generate sample scientific data
np.random.seed(42)
time = np.linspace(0, 10, 1000)
signal1 = np.sin(2 * np.pi * time) * np.exp(-time/5) + np.random.normal(0, 0.05, 1000)
signal2 = np.cos(2 * np.pi * time) * np.exp(-time/7) + np.random.normal(0, 0.05, 1000)

# Main plot - Time series with confidence bands
ax1 = fig.add_subplot(gs[:2, 0])

# Calculate rolling statistics
window = 50
mean1 = pd.Series(signal1).rolling(window=window).mean()
std1 = pd.Series(signal1).rolling(window=window).std()

# Plot with confidence bands
ax1.fill_between(time, mean1 - 2*std1, mean1 + 2*std1, 
                 color=colors['primary'], alpha=0.2, label='95% CI')
ax1.plot(time, signal1, color=colors['primary'], alpha=0.3, linewidth=0.5)
ax1.plot(time, mean1, color=colors['primary'], linewidth=2.5, label='Signal A')
ax1.plot(time, signal2, color=colors['secondary'], linewidth=2.5, 
         linestyle='--', label='Signal B')

# Annotations
peak_idx = np.argmax(mean1.dropna())
ax1.annotate('Peak Response',
            xy=(time[peak_idx], mean1.iloc[peak_idx]),
            xytext=(time[peak_idx] + 2, mean1.iloc[peak_idx] + 0.3),
            arrowprops=dict(arrowstyle='-|>',
                          connectionstyle='arc3,rad=0.3',
                          color=colors['dark'],
                          lw=1.5),
            fontsize=10,
            bbox=dict(boxstyle="round,pad=0.3",
                     facecolor=colors['highlight'],
                     edgecolor=colors['dark'],
                     alpha=0.8))

# Highlight regions
ax1.axvspan(2, 4, alpha=0.1, color=colors['tertiary'], label='Critical Period')

# Styling
ax1.set_title('Temporal Evolution of System Response', 
             fontsize=14, fontweight='bold', pad=15)
ax1.set_xlabel('Time (s)', fontsize=11)
ax1.set_ylabel('Amplitude (a.u.)', fontsize=11)
ax1.grid(True, alpha=0.3, linestyle='--')
ax1.legend(loc='upper right', frameon=True, fancybox=True, 
          shadow=True, fontsize=9)

# Spectral analysis
ax2 = fig.add_subplot(gs[0, 1])
freqs = np.fft.fftfreq(len(signal1), d=time[1]-time[0])
fft1 = np.abs(np.fft.fft(signal1))
mask = freqs > 0

ax2.semilogy(freqs[mask], fft1[mask], color=colors['primary'], 
            linewidth=1.5, label='Signal A')
ax2.fill_between(freqs[mask], 1e-3, fft1[mask], 
                color=colors['primary'], alpha=0.3)

ax2.set_title('Frequency Spectrum', fontsize=12, fontweight='bold')
ax2.set_xlabel('Frequency (Hz)', fontsize=10)
ax2.set_ylabel('Power', fontsize=10)
ax2.grid(True, which='both', alpha=0.3)
ax2.set_xlim(0, 5)

# Phase space plot
ax3 = fig.add_subplot(gs[0, 2])
ax3.scatter(signal1[:-1], signal1[1:], c=time[:-1], 
           cmap='viridis', s=1, alpha=0.5)
ax3.set_title('Phase Space', fontsize=12, fontweight='bold')
ax3.set_xlabel('x(t)', fontsize=10)
ax3.set_ylabel('x(t+1)', fontsize=10)
ax3.grid(True, alpha=0.3)

# Distribution plot
ax4 = fig.add_subplot(gs[1, 1])
counts, bins, patches = ax4.hist(signal1, bins=50, 
                                 density=True, alpha=0.7,
                                 color=colors['primary'],
                                 edgecolor='black', linewidth=0.5)

# Fit and plot normal distribution
from scipy import stats
mu, sigma = signal1.mean(), signal1.std()
x_fit = np.linspace(signal1.min(), signal1.max(), 100)
ax4.plot(x_fit, stats.norm.pdf(x_fit, mu, sigma), 
        color=colors['secondary'], linewidth=2, 
        linestyle='--', label=f'μ={mu:.2f}, σ={sigma:.2f}')

ax4.set_title('Distribution', fontsize=12, fontweight='bold')
ax4.set_xlabel('Value', fontsize=10)
ax4.set_ylabel('Density', fontsize=10)
ax4.legend(fontsize=9)
ax4.grid(True, alpha=0.3, axis='y')

# Correlation plot
ax5 = fig.add_subplot(gs[1, 2])
correlation = np.corrcoef(signal1[:500].reshape(50, 10).T)
im = ax5.imshow(correlation, cmap='RdBu_r', vmin=-1, vmax=1)
ax5.set_title('Correlation Matrix', fontsize=12, fontweight='bold')
ax5.set_xlabel('Component', fontsize=10)
ax5.set_ylabel('Component', fontsize=10)

# Add colorbar
cbar = plt.colorbar(im, ax=ax5, fraction=0.046, pad=0.04)
cbar.set_label('Correlation', fontsize=10)

# Statistical summary table
ax6 = fig.add_subplot(gs[2, :])
ax6.axis('tight')
ax6.axis('off')

# Create summary statistics
stats_data = {
    'Metric': ['Mean', 'Std Dev', 'Skewness', 'Kurtosis', 'Max', 'Min'],
    'Signal A': [f'{signal1.mean():.3f}', f'{signal1.std():.3f}', 
                f'{stats.skew(signal1):.3f}', f'{stats.kurtosis(signal1):.3f}',
                f'{signal1.max():.3f}', f'{signal1.min():.3f}'],
    'Signal B': [f'{signal2.mean():.3f}', f'{signal2.std():.3f}',
                f'{stats.skew(signal2):.3f}', f'{stats.kurtosis(signal2):.3f}',
                f'{signal2.max():.3f}', f'{signal2.min():.3f}']
}

# Create table
table_data = list(zip(stats_data['Metric'], 
                     stats_data['Signal A'], 
                     stats_data['Signal B']))

table = ax6.table(cellText=table_data,
                 colLabels=['Metric', 'Signal A', 'Signal B'],
                 cellLoc='center',
                 loc='center',
                 colWidths=[0.15, 0.15, 0.15])

# Style the table
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1, 1.5)

# Color header
for i in range(3):
    table[(0, i)].set_facecolor(colors['primary'])
    table[(0, i)].set_text_props(weight='bold', color='white')

# Alternate row colors
for i in range(1, len(stats_data['Metric']) + 1):
    for j in range(3):
        if i % 2 == 0:
            table[(i, j)].set_facecolor(colors['light'])

# Add figure title and metadata
fig.suptitle('Comprehensive Signal Analysis Report', 
            fontsize=16, fontweight='bold', y=0.98)

# Add footer with metadata
fig.text(0.99, 0.01, 
        f'Generated: {pd.Timestamp.now().strftime("%Y-%m-%d %H:%M")} | ' +
        f'Data points: {len(signal1)} | ' + 
        f'Sampling rate: {1/(time[1]-time[0]):.1f} Hz',
        fontsize=8, ha='right', style='italic', color='gray')

# Add data source
fig.text(0.01, 0.01,
        'Source: Experimental Dataset #2024-A1',
        fontsize=8, ha='left', style='italic', color='gray')

# Add custom legend for the entire figure
legend_elements = [
    mpatches.Patch(color=colors['primary'], label='Primary Signal'),
    mpatches.Patch(color=colors['secondary'], label='Secondary Signal'),
    mpatches.Patch(color=colors['tertiary'], alpha=0.3, label='Region of Interest'),
]
fig.legend(handles=legend_elements, 
          loc='upper center', 
          bbox_to_anchor=(0.5, 0.95),
          ncol=3,
          frameon=True,
          fancybox=True,
          shadow=True,
          fontsize=9)

# Adjust layout to prevent overlap
plt.tight_layout(rect=[0, 0.02, 1, 0.93])

# Save figure
# plt.savefig('publication_figure.pdf', dpi=300, bbox_inches='tight')
# plt.savefig('publication_figure.png', dpi=300, bbox_inches='tight', 
#            facecolor='white', edgecolor='none')

plt.show()

💡 Pro Tips for Plot Customization

⚠️ Common Customization Mistakes