590 lines
13 KiB
Markdown
590 lines
13 KiB
Markdown
# Matplotlib Styling Guide
|
|
|
|
Comprehensive guide for styling and customizing matplotlib visualizations.
|
|
|
|
## Colormaps
|
|
|
|
### Colormap Categories
|
|
|
|
**1. Perceptually Uniform Sequential**
|
|
Best for ordered data that progresses from low to high values.
|
|
- `viridis` (default, colorblind-friendly)
|
|
- `plasma`
|
|
- `inferno`
|
|
- `magma`
|
|
- `cividis` (optimized for colorblind viewers)
|
|
|
|
**Usage:**
|
|
```python
|
|
im = ax.imshow(data, cmap='viridis')
|
|
scatter = ax.scatter(x, y, c=values, cmap='plasma')
|
|
```
|
|
|
|
**2. Sequential**
|
|
Traditional colormaps for ordered data.
|
|
- `Blues`, `Greens`, `Reds`, `Oranges`, `Purples`
|
|
- `YlOrBr`, `YlOrRd`, `OrRd`, `PuRd`
|
|
- `BuPu`, `GnBu`, `PuBu`, `YlGnBu`
|
|
|
|
**3. Diverging**
|
|
Best for data with a meaningful center point (e.g., zero, mean).
|
|
- `coolwarm` (blue to red)
|
|
- `RdBu` (red-blue)
|
|
- `RdYlBu` (red-yellow-blue)
|
|
- `RdYlGn` (red-yellow-green)
|
|
- `PiYG`, `PRGn`, `BrBG`, `PuOr`, `RdGy`
|
|
|
|
**Usage:**
|
|
```python
|
|
# Center colormap at zero
|
|
im = ax.imshow(data, cmap='coolwarm', vmin=-1, vmax=1)
|
|
```
|
|
|
|
**4. Qualitative**
|
|
Best for categorical/nominal data without inherent ordering.
|
|
- `tab10` (10 distinct colors)
|
|
- `tab20` (20 distinct colors)
|
|
- `Set1`, `Set2`, `Set3`
|
|
- `Pastel1`, `Pastel2`
|
|
- `Dark2`, `Accent`, `Paired`
|
|
|
|
**Usage:**
|
|
```python
|
|
colors = plt.cm.tab10(np.linspace(0, 1, n_categories))
|
|
for i, category in enumerate(categories):
|
|
ax.plot(x, y[i], color=colors[i], label=category)
|
|
```
|
|
|
|
**5. Cyclic**
|
|
Best for cyclic data (e.g., phase, angle).
|
|
- `twilight`
|
|
- `twilight_shifted`
|
|
- `hsv`
|
|
|
|
### Colormap Best Practices
|
|
|
|
1. **Avoid `jet` colormap** - Not perceptually uniform, misleading
|
|
2. **Use perceptually uniform colormaps** - `viridis`, `plasma`, `cividis`
|
|
3. **Consider colorblind users** - Use `viridis`, `cividis`, or test with colorblind simulators
|
|
4. **Match colormap to data type**:
|
|
- Sequential: increasing/decreasing data
|
|
- Diverging: data with meaningful center
|
|
- Qualitative: categories
|
|
5. **Reverse colormaps** - Add `_r` suffix: `viridis_r`, `coolwarm_r`
|
|
|
|
### Creating Custom Colormaps
|
|
|
|
```python
|
|
from matplotlib.colors import LinearSegmentedColormap
|
|
|
|
# From color list
|
|
colors = ['blue', 'white', 'red']
|
|
n_bins = 100
|
|
cmap = LinearSegmentedColormap.from_list('custom', colors, N=n_bins)
|
|
|
|
# From RGB values
|
|
colors = [(0, 0, 1), (1, 1, 1), (1, 0, 0)] # RGB tuples
|
|
cmap = LinearSegmentedColormap.from_list('custom', colors)
|
|
|
|
# Use the custom colormap
|
|
ax.imshow(data, cmap=cmap)
|
|
```
|
|
|
|
### Discrete Colormaps
|
|
|
|
```python
|
|
import matplotlib.colors as mcolors
|
|
|
|
# Create discrete colormap from continuous
|
|
cmap = plt.cm.viridis
|
|
bounds = np.linspace(0, 10, 11)
|
|
norm = mcolors.BoundaryNorm(bounds, cmap.N)
|
|
im = ax.imshow(data, cmap=cmap, norm=norm)
|
|
```
|
|
|
|
## Style Sheets
|
|
|
|
### Using Built-in Styles
|
|
|
|
```python
|
|
# List available styles
|
|
print(plt.style.available)
|
|
|
|
# Apply a style
|
|
plt.style.use('seaborn-v0_8-darkgrid')
|
|
|
|
# Apply multiple styles (later styles override earlier ones)
|
|
plt.style.use(['seaborn-v0_8-whitegrid', 'seaborn-v0_8-poster'])
|
|
|
|
# Temporarily use a style
|
|
with plt.style.context('ggplot'):
|
|
fig, ax = plt.subplots()
|
|
ax.plot(x, y)
|
|
```
|
|
|
|
### Popular Built-in Styles
|
|
|
|
- `default` - Matplotlib's default style
|
|
- `classic` - Classic matplotlib look (pre-2.0)
|
|
- `seaborn-v0_8-*` - Seaborn-inspired styles
|
|
- `seaborn-v0_8-darkgrid`, `seaborn-v0_8-whitegrid`
|
|
- `seaborn-v0_8-dark`, `seaborn-v0_8-white`
|
|
- `seaborn-v0_8-ticks`, `seaborn-v0_8-poster`, `seaborn-v0_8-talk`
|
|
- `ggplot` - ggplot2-inspired style
|
|
- `bmh` - Bayesian Methods for Hackers style
|
|
- `fivethirtyeight` - FiveThirtyEight style
|
|
- `grayscale` - Grayscale style
|
|
|
|
### Creating Custom Style Sheets
|
|
|
|
Create a file named `custom_style.mplstyle`:
|
|
|
|
```
|
|
# custom_style.mplstyle
|
|
|
|
# Figure
|
|
figure.figsize: 10, 6
|
|
figure.dpi: 100
|
|
figure.facecolor: white
|
|
|
|
# Font
|
|
font.family: sans-serif
|
|
font.sans-serif: Arial, Helvetica
|
|
font.size: 12
|
|
|
|
# Axes
|
|
axes.labelsize: 14
|
|
axes.titlesize: 16
|
|
axes.facecolor: white
|
|
axes.edgecolor: black
|
|
axes.linewidth: 1.5
|
|
axes.grid: True
|
|
axes.axisbelow: True
|
|
|
|
# Grid
|
|
grid.color: gray
|
|
grid.linestyle: --
|
|
grid.linewidth: 0.5
|
|
grid.alpha: 0.3
|
|
|
|
# Lines
|
|
lines.linewidth: 2
|
|
lines.markersize: 8
|
|
|
|
# Ticks
|
|
xtick.labelsize: 10
|
|
ytick.labelsize: 10
|
|
xtick.direction: in
|
|
ytick.direction: in
|
|
xtick.major.size: 6
|
|
ytick.major.size: 6
|
|
xtick.minor.size: 3
|
|
ytick.minor.size: 3
|
|
|
|
# Legend
|
|
legend.fontsize: 12
|
|
legend.frameon: True
|
|
legend.framealpha: 0.8
|
|
legend.fancybox: True
|
|
|
|
# Savefig
|
|
savefig.dpi: 300
|
|
savefig.bbox: tight
|
|
savefig.facecolor: white
|
|
```
|
|
|
|
Load and use:
|
|
```python
|
|
plt.style.use('path/to/custom_style.mplstyle')
|
|
```
|
|
|
|
## rcParams Configuration
|
|
|
|
### Global Configuration
|
|
|
|
```python
|
|
import matplotlib.pyplot as plt
|
|
|
|
# Configure globally
|
|
plt.rcParams['figure.figsize'] = (10, 6)
|
|
plt.rcParams['font.size'] = 12
|
|
plt.rcParams['axes.labelsize'] = 14
|
|
|
|
# Or update multiple at once
|
|
plt.rcParams.update({
|
|
'figure.figsize': (10, 6),
|
|
'font.size': 12,
|
|
'axes.labelsize': 14,
|
|
'axes.titlesize': 16,
|
|
'lines.linewidth': 2
|
|
})
|
|
```
|
|
|
|
### Temporary Configuration
|
|
|
|
```python
|
|
# Context manager for temporary changes
|
|
with plt.rc_context({'font.size': 14, 'lines.linewidth': 2.5}):
|
|
fig, ax = plt.subplots()
|
|
ax.plot(x, y)
|
|
```
|
|
|
|
### Common rcParams
|
|
|
|
**Figure settings:**
|
|
```python
|
|
plt.rcParams['figure.figsize'] = (10, 6)
|
|
plt.rcParams['figure.dpi'] = 100
|
|
plt.rcParams['figure.facecolor'] = 'white'
|
|
plt.rcParams['figure.edgecolor'] = 'white'
|
|
plt.rcParams['figure.autolayout'] = False
|
|
plt.rcParams['figure.constrained_layout.use'] = True
|
|
```
|
|
|
|
**Font settings:**
|
|
```python
|
|
plt.rcParams['font.family'] = 'sans-serif'
|
|
plt.rcParams['font.sans-serif'] = ['Arial', 'Helvetica', 'DejaVu Sans']
|
|
plt.rcParams['font.size'] = 12
|
|
plt.rcParams['font.weight'] = 'normal'
|
|
```
|
|
|
|
**Axes settings:**
|
|
```python
|
|
plt.rcParams['axes.facecolor'] = 'white'
|
|
plt.rcParams['axes.edgecolor'] = 'black'
|
|
plt.rcParams['axes.linewidth'] = 1.5
|
|
plt.rcParams['axes.grid'] = True
|
|
plt.rcParams['axes.labelsize'] = 14
|
|
plt.rcParams['axes.titlesize'] = 16
|
|
plt.rcParams['axes.labelweight'] = 'normal'
|
|
plt.rcParams['axes.spines.top'] = True
|
|
plt.rcParams['axes.spines.right'] = True
|
|
```
|
|
|
|
**Line settings:**
|
|
```python
|
|
plt.rcParams['lines.linewidth'] = 2
|
|
plt.rcParams['lines.linestyle'] = '-'
|
|
plt.rcParams['lines.marker'] = 'None'
|
|
plt.rcParams['lines.markersize'] = 6
|
|
```
|
|
|
|
**Save settings:**
|
|
```python
|
|
plt.rcParams['savefig.dpi'] = 300
|
|
plt.rcParams['savefig.format'] = 'png'
|
|
plt.rcParams['savefig.bbox'] = 'tight'
|
|
plt.rcParams['savefig.pad_inches'] = 0.1
|
|
plt.rcParams['savefig.transparent'] = False
|
|
```
|
|
|
|
## Color Palettes
|
|
|
|
### Named Color Sets
|
|
|
|
```python
|
|
# Tableau colors
|
|
tableau_colors = plt.cm.tab10.colors
|
|
|
|
# CSS4 colors (subset)
|
|
css_colors = ['steelblue', 'coral', 'teal', 'goldenrod', 'crimson']
|
|
|
|
# Manual definition
|
|
custom_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
|
|
```
|
|
|
|
### Color Cycles
|
|
|
|
```python
|
|
# Set default color cycle
|
|
from cycler import cycler
|
|
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
|
|
plt.rcParams['axes.prop_cycle'] = cycler(color=colors)
|
|
|
|
# Or combine color and line style
|
|
plt.rcParams['axes.prop_cycle'] = cycler(color=colors) + cycler(linestyle=['-', '--', ':', '-.'])
|
|
```
|
|
|
|
### Palette Generation
|
|
|
|
```python
|
|
# Evenly spaced colors from colormap
|
|
n_colors = 5
|
|
colors = plt.cm.viridis(np.linspace(0, 1, n_colors))
|
|
|
|
# Use in plot
|
|
for i, (x, y) in enumerate(data):
|
|
ax.plot(x, y, color=colors[i])
|
|
```
|
|
|
|
## Typography
|
|
|
|
### Font Configuration
|
|
|
|
```python
|
|
# Set font family
|
|
plt.rcParams['font.family'] = 'serif'
|
|
plt.rcParams['font.serif'] = ['Times New Roman', 'DejaVu Serif']
|
|
|
|
# Or sans-serif
|
|
plt.rcParams['font.family'] = 'sans-serif'
|
|
plt.rcParams['font.sans-serif'] = ['Arial', 'Helvetica']
|
|
|
|
# Or monospace
|
|
plt.rcParams['font.family'] = 'monospace'
|
|
plt.rcParams['font.monospace'] = ['Courier New', 'DejaVu Sans Mono']
|
|
```
|
|
|
|
### Font Properties in Text
|
|
|
|
```python
|
|
from matplotlib import font_manager
|
|
|
|
# Specify font properties
|
|
ax.text(x, y, 'Text',
|
|
fontsize=14,
|
|
fontweight='bold', # 'normal', 'bold', 'heavy', 'light'
|
|
fontstyle='italic', # 'normal', 'italic', 'oblique'
|
|
fontfamily='serif')
|
|
|
|
# Use specific font file
|
|
prop = font_manager.FontProperties(fname='path/to/font.ttf')
|
|
ax.text(x, y, 'Text', fontproperties=prop)
|
|
```
|
|
|
|
### Mathematical Text
|
|
|
|
```python
|
|
# LaTeX-style math
|
|
ax.set_title(r'$\alpha > \beta$')
|
|
ax.set_xlabel(r'$\mu \pm \sigma$')
|
|
ax.text(x, y, r'$\int_0^\infty e^{-x} dx = 1$')
|
|
|
|
# Subscripts and superscripts
|
|
ax.set_ylabel(r'$y = x^2 + 2x + 1$')
|
|
ax.text(x, y, r'$x_1, x_2, \ldots, x_n$')
|
|
|
|
# Greek letters
|
|
ax.text(x, y, r'$\alpha, \beta, \gamma, \delta, \epsilon$')
|
|
```
|
|
|
|
### Using Full LaTeX
|
|
|
|
```python
|
|
# Enable full LaTeX rendering (requires LaTeX installation)
|
|
plt.rcParams['text.usetex'] = True
|
|
plt.rcParams['text.latex.preamble'] = r'\usepackage{amsmath}'
|
|
|
|
ax.set_title(r'\textbf{Bold Title}')
|
|
ax.set_xlabel(r'Time $t$ (s)')
|
|
```
|
|
|
|
## Spines and Grids
|
|
|
|
### Spine Customization
|
|
|
|
```python
|
|
# Hide specific spines
|
|
ax.spines['top'].set_visible(False)
|
|
ax.spines['right'].set_visible(False)
|
|
|
|
# Move spine position
|
|
ax.spines['left'].set_position(('outward', 10))
|
|
ax.spines['bottom'].set_position(('data', 0))
|
|
|
|
# Change spine color and width
|
|
ax.spines['left'].set_color('red')
|
|
ax.spines['bottom'].set_linewidth(2)
|
|
```
|
|
|
|
### Grid Customization
|
|
|
|
```python
|
|
# Basic grid
|
|
ax.grid(True)
|
|
|
|
# Customized grid
|
|
ax.grid(True, which='major', linestyle='--', linewidth=0.8, alpha=0.3)
|
|
ax.grid(True, which='minor', linestyle=':', linewidth=0.5, alpha=0.2)
|
|
|
|
# Grid for specific axis
|
|
ax.grid(True, axis='x') # Only vertical lines
|
|
ax.grid(True, axis='y') # Only horizontal lines
|
|
|
|
# Grid behind or in front of data
|
|
ax.set_axisbelow(True) # Grid behind data
|
|
```
|
|
|
|
## Legend Customization
|
|
|
|
### Legend Positioning
|
|
|
|
```python
|
|
# Location strings
|
|
ax.legend(loc='best') # Automatic best position
|
|
ax.legend(loc='upper right')
|
|
ax.legend(loc='upper left')
|
|
ax.legend(loc='lower right')
|
|
ax.legend(loc='lower left')
|
|
ax.legend(loc='center')
|
|
ax.legend(loc='upper center')
|
|
ax.legend(loc='lower center')
|
|
ax.legend(loc='center left')
|
|
ax.legend(loc='center right')
|
|
|
|
# Precise positioning (bbox_to_anchor)
|
|
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') # Outside plot area
|
|
ax.legend(bbox_to_anchor=(0.5, -0.15), loc='upper center', ncol=3) # Below plot
|
|
```
|
|
|
|
### Legend Styling
|
|
|
|
```python
|
|
ax.legend(
|
|
fontsize=12,
|
|
frameon=True, # Show frame
|
|
framealpha=0.9, # Frame transparency
|
|
fancybox=True, # Rounded corners
|
|
shadow=True, # Shadow effect
|
|
ncol=2, # Number of columns
|
|
title='Legend Title', # Legend title
|
|
title_fontsize=14, # Title font size
|
|
edgecolor='black', # Frame edge color
|
|
facecolor='white' # Frame background color
|
|
)
|
|
```
|
|
|
|
### Custom Legend Entries
|
|
|
|
```python
|
|
from matplotlib.lines import Line2D
|
|
|
|
# Create custom legend handles
|
|
custom_lines = [Line2D([0], [0], color='red', lw=2),
|
|
Line2D([0], [0], color='blue', lw=2, linestyle='--'),
|
|
Line2D([0], [0], marker='o', color='w', markerfacecolor='green', markersize=10)]
|
|
|
|
ax.legend(custom_lines, ['Label 1', 'Label 2', 'Label 3'])
|
|
```
|
|
|
|
## Layout and Spacing
|
|
|
|
### Constrained Layout
|
|
|
|
```python
|
|
# Preferred method (automatic adjustment)
|
|
fig, axes = plt.subplots(2, 2, constrained_layout=True)
|
|
```
|
|
|
|
### Tight Layout
|
|
|
|
```python
|
|
# Alternative method
|
|
fig, axes = plt.subplots(2, 2)
|
|
plt.tight_layout(pad=1.5, h_pad=2.0, w_pad=2.0)
|
|
```
|
|
|
|
### Manual Adjustment
|
|
|
|
```python
|
|
# Fine-grained control
|
|
plt.subplots_adjust(left=0.1, right=0.9, top=0.9, bottom=0.1,
|
|
hspace=0.3, wspace=0.4)
|
|
```
|
|
|
|
## Professional Publication Style
|
|
|
|
Example configuration for publication-quality figures:
|
|
|
|
```python
|
|
# Publication style configuration
|
|
plt.rcParams.update({
|
|
# Figure
|
|
'figure.figsize': (8, 6),
|
|
'figure.dpi': 100,
|
|
'savefig.dpi': 300,
|
|
'savefig.bbox': 'tight',
|
|
'savefig.pad_inches': 0.1,
|
|
|
|
# Font
|
|
'font.family': 'sans-serif',
|
|
'font.sans-serif': ['Arial', 'Helvetica'],
|
|
'font.size': 11,
|
|
|
|
# Axes
|
|
'axes.labelsize': 12,
|
|
'axes.titlesize': 14,
|
|
'axes.linewidth': 1.5,
|
|
'axes.grid': False,
|
|
'axes.spines.top': False,
|
|
'axes.spines.right': False,
|
|
|
|
# Lines
|
|
'lines.linewidth': 2,
|
|
'lines.markersize': 8,
|
|
|
|
# Ticks
|
|
'xtick.labelsize': 10,
|
|
'ytick.labelsize': 10,
|
|
'xtick.major.size': 6,
|
|
'ytick.major.size': 6,
|
|
'xtick.major.width': 1.5,
|
|
'ytick.major.width': 1.5,
|
|
'xtick.direction': 'in',
|
|
'ytick.direction': 'in',
|
|
|
|
# Legend
|
|
'legend.fontsize': 10,
|
|
'legend.frameon': True,
|
|
'legend.framealpha': 1.0,
|
|
'legend.edgecolor': 'black'
|
|
})
|
|
```
|
|
|
|
## Dark Theme
|
|
|
|
```python
|
|
# Dark background style
|
|
plt.style.use('dark_background')
|
|
|
|
# Or manual configuration
|
|
plt.rcParams.update({
|
|
'figure.facecolor': '#1e1e1e',
|
|
'axes.facecolor': '#1e1e1e',
|
|
'axes.edgecolor': 'white',
|
|
'axes.labelcolor': 'white',
|
|
'text.color': 'white',
|
|
'xtick.color': 'white',
|
|
'ytick.color': 'white',
|
|
'grid.color': 'gray',
|
|
'legend.facecolor': '#1e1e1e',
|
|
'legend.edgecolor': 'white'
|
|
})
|
|
```
|
|
|
|
## Color Accessibility
|
|
|
|
### Colorblind-Friendly Palettes
|
|
|
|
```python
|
|
# Use colorblind-friendly colormaps
|
|
colorblind_friendly = ['viridis', 'plasma', 'cividis']
|
|
|
|
# Colorblind-friendly discrete colors
|
|
cb_colors = ['#0173B2', '#DE8F05', '#029E73', '#CC78BC',
|
|
'#CA9161', '#949494', '#ECE133', '#56B4E9']
|
|
|
|
# Test with simulation tools or use these validated palettes
|
|
```
|
|
|
|
### High Contrast
|
|
|
|
```python
|
|
# Ensure sufficient contrast
|
|
plt.rcParams['axes.edgecolor'] = 'black'
|
|
plt.rcParams['axes.linewidth'] = 2
|
|
plt.rcParams['xtick.major.width'] = 2
|
|
plt.rcParams['ytick.major.width'] = 2
|
|
```
|