Transform basic charts into stunning, publication-ready visualizations
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!
Experiment with different customization options in real-time:
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()
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()
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()
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()
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',
}
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()
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()