How to Use Colormaps in Matplotlib
Colormaps determine how numerical values map to colors in your visualizations. The wrong colormap can hide patterns, create false features, or make your plots inaccessible to colorblind viewers. The...
Key Insights
- Colormaps fundamentally change how viewers interpret your data—choosing between sequential, diverging, and qualitative types depends on whether your data has ordering, a meaningful center point, or categorical distinctions.
- Matplotlib’s perceptually uniform colormaps like ‘viridis’ and ‘plasma’ should be your default choice because they maintain consistent visual intensity across the entire range and remain interpretable when printed in grayscale.
- Always add colorbars with clear labels and consider logarithmic normalization for data spanning multiple orders of magnitude—proper scaling prevents your colormap from obscuring important patterns in your data.
Introduction to Colormaps
Colormaps determine how numerical values map to colors in your visualizations. The wrong colormap can hide patterns, create false features, or make your plots inaccessible to colorblind viewers. The right one reveals structure in your data immediately.
Matplotlib organizes colormaps into three fundamental types based on data characteristics:
Sequential colormaps progress from light to dark (or vice versa) in a single hue or across multiple hues. Use these for data that goes from low to high values without a meaningful middle point—think elevation maps, probability distributions, or temperature readings.
Diverging colormaps have a neutral color in the middle and contrasting colors at both ends. These work when your data has a critical center point—correlation coefficients around zero, temperature anomalies relative to average, or financial data showing gains and losses.
Qualitative colormaps provide distinct colors for categorical data without implying order. Use these for labeling different classes, regions, or groups where no category is “greater” than another.
Here’s how colormap choice affects interpretation:
import matplotlib.pyplot as plt
import numpy as np
# Generate sample data with clear structure
data = np.random.randn(10, 10).cumsum(axis=1)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Same data, three different colormaps
axes[0].imshow(data, cmap='viridis')
axes[0].set_title('Sequential: viridis')
axes[1].imshow(data, cmap='RdBu')
axes[1].set_title('Diverging: RdBu')
axes[2].imshow(data, cmap='tab10')
axes[2].set_title('Qualitative: tab10 (wrong choice)')
plt.tight_layout()
plt.show()
The qualitative colormap creates false boundaries and obscures the continuous nature of the data. This isn’t academic—I’ve seen production dashboards where poor colormap choices led to misinterpretation of critical metrics.
Built-in Colormap Categories
Matplotlib ships with over 150 colormaps organized into categories. Here’s what you need to know about each:
Perceptually Uniform Sequential (‘viridis’, ‘plasma’, ‘inferno’, ‘magma’, ‘cividis’): These maintain consistent perceptual increments across the range. A 10-unit increase in data value produces the same perceived color change whether you’re at the low or high end. ‘viridis’ became the default colormap in Matplotlib 2.0 for good reason—it works in grayscale and for most types of colorblindness.
Sequential (‘Blues’, ‘Greens’, ‘Reds’, ‘YlOrRd’): Single or multi-hue progressions. Useful when you want to match your institution’s colors, but less perceptually uniform than the previous category.
Diverging (‘RdBu’, ‘coolwarm’, ‘seismic’, ‘bwr’): Critical for data with meaningful zero points. ‘coolwarm’ is perceptually uniform; ‘seismic’ is not.
Cyclic (’twilight’, ‘hsv’): For periodic data like angles, phases, or times of day where the maximum value wraps back to the minimum.
Qualitative (’tab10’, ‘Set1’, ‘Paired’): Maximally distinct colors for categorical data. ’tab10’ gives you 10 colors; ’tab20’ gives 20.
Here’s how to explore available colormaps:
import matplotlib.pyplot as plt
import numpy as np
# Get all colormap names by category
categories = {
'Perceptually Uniform': ['viridis', 'plasma', 'inferno', 'magma', 'cividis'],
'Sequential': ['Blues', 'Greens', 'Oranges', 'Reds'],
'Diverging': ['RdBu', 'coolwarm', 'seismic', 'bwr'],
'Qualitative': ['tab10', 'Set1', 'Set2', 'Paired']
}
gradient = np.linspace(0, 1, 256).reshape(1, -1)
fig, axes = plt.subplots(len(categories), 1, figsize=(10, 8))
for ax, (category, cmaps) in zip(axes, categories.items()):
ax.set_title(category, fontsize=12, fontweight='bold', loc='left')
ax.set_yticks([])
ax.set_xticks([])
# Display first colormap as example
ax.imshow(gradient, aspect='auto', cmap=cmaps[0])
plt.tight_layout()
plt.show()
Applying Colormaps to Different Plot Types
The cmap parameter works consistently across Matplotlib’s plot types, but the data structure differs.
Scatter plots require a separate array for color values:
import matplotlib.pyplot as plt
import numpy as np
# Generate correlated data
np.random.seed(42)
x = np.random.randn(500)
y = x + np.random.randn(500) * 0.5
colors = x + y # Color based on sum
plt.figure(figsize=(8, 6))
scatter = plt.scatter(x, y, c=colors, cmap='coolwarm', s=50, alpha=0.6)
plt.colorbar(scatter, label='x + y value')
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Scatter Plot with Continuous Color Scale')
plt.show()
Heatmaps via imshow() apply colormaps to 2D arrays directly:
# Create correlation-style matrix
data = np.random.randn(20, 20)
correlation = np.corrcoef(data)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# Poor choice: sequential for diverging data
im1 = ax1.imshow(correlation, cmap='viridis', vmin=-1, vmax=1)
ax1.set_title('Wrong: Sequential colormap')
plt.colorbar(im1, ax=ax1)
# Correct: diverging colormap centered at zero
im2 = ax2.imshow(correlation, cmap='RdBu_r', vmin=-1, vmax=1)
ax2.set_title('Correct: Diverging colormap')
plt.colorbar(im2, ax=ax2)
plt.tight_layout()
plt.show()
Contour plots work with both filled (contourf) and line (contour) variants:
# Create 2D function with interesting structure
x = np.linspace(-3, 3, 100)
y = np.linspace(-3, 3, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(X) * np.cos(Y) + 0.1 * X
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
contour = plt.contourf(X, Y, Z, levels=20, cmap='RdBu')
plt.colorbar(contour, label='Function value')
plt.title('Filled Contour Plot')
plt.subplot(1, 2, 2)
contour_lines = plt.contour(X, Y, Z, levels=20, cmap='viridis')
plt.colorbar(contour_lines, label='Function value')
plt.title('Line Contour Plot')
plt.tight_layout()
plt.show()
Customizing and Creating Custom Colormaps
Append _r to any colormap name to reverse it: 'viridis_r', 'RdBu_r'. This is essential for diverging colormaps where you need to match color to meaning (red for hot/positive, blue for cold/negative).
Control color mapping with vmin and vmax to clip or expand the range:
data = np.random.randn(50, 50)
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Default range
axes[0].imshow(data, cmap='viridis')
axes[0].set_title('Default range')
# Clipped range emphasizes middle values
axes[1].imshow(data, cmap='viridis', vmin=-1, vmax=1)
axes[1].set_title('Clipped: vmin=-1, vmax=1')
# Expanded range compresses color variation
axes[2].imshow(data, cmap='viridis', vmin=-5, vmax=5)
axes[2].set_title('Expanded: vmin=-5, vmax=5')
plt.tight_layout()
plt.show()
Create custom colormaps from brand colors or specific requirements:
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
# Method 1: ListedColormap for discrete colors
custom_colors = ['#2E86AB', '#A23B72', '#F18F01', '#C73E1D']
discrete_cmap = ListedColormap(custom_colors)
# Method 2: LinearSegmentedColormap for smooth gradients
colors = ['darkblue', 'lightblue', 'yellow', 'red']
n_bins = 256
smooth_cmap = LinearSegmentedColormap.from_list('custom', colors, N=n_bins)
# Apply to data
data = np.random.rand(20, 20)
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
im1 = ax1.imshow(data, cmap=discrete_cmap)
ax1.set_title('Discrete Custom Colormap')
plt.colorbar(im1, ax=ax1)
im2 = ax2.imshow(data, cmap=smooth_cmap)
ax2.set_title('Smooth Custom Colormap')
plt.colorbar(im2, ax=ax2)
plt.tight_layout()
plt.show()
Colorbars and Normalization
A plot without a colorbar is like a chart without axis labels—technically complete but practically useless. Always add colorbars with descriptive labels.
For data spanning orders of magnitude, linear colormaps compress variation in smaller values. Use logarithmic or power normalization:
from matplotlib.colors import LogNorm, PowerNorm
# Generate data spanning multiple orders of magnitude
data = np.random.exponential(scale=1.0, size=(50, 50))
data = data ** 2 # Increase range
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
# Linear scale (poor for this data)
im1 = axes[0].imshow(data, cmap='viridis')
axes[0].set_title('Linear scale')
cbar1 = plt.colorbar(im1, ax=axes[0])
cbar1.set_label('Value', rotation=270, labelpad=20)
# Logarithmic scale
im2 = axes[1].imshow(data, cmap='viridis', norm=LogNorm())
axes[1].set_title('Logarithmic scale')
cbar2 = plt.colorbar(im2, ax=axes[1])
cbar2.set_label('Value (log)', rotation=270, labelpad=20)
# Power normalization (gamma=0.5 is sqrt)
im3 = axes[2].imshow(data, cmap='viridis', norm=PowerNorm(gamma=0.5))
axes[2].set_title('Power scale (gamma=0.5)')
cbar3 = plt.colorbar(im3, ax=axes[2])
cbar3.set_label('Value (sqrt)', rotation=270, labelpad=20)
plt.tight_layout()
plt.show()
Customize colorbar ticks and formatting for clarity:
import matplotlib.ticker as ticker
data = np.random.rand(30, 30) * 1000
fig, ax = plt.subplots(figsize=(8, 6))
im = ax.imshow(data, cmap='plasma')
# Create colorbar with custom properties
cbar = plt.colorbar(im, ax=ax, extend='both', shrink=0.8)
cbar.set_label('Measurement (units)', rotation=270, labelpad=25, fontsize=12)
cbar.ax.yaxis.set_major_formatter(ticker.FormatStrFormatter('%.0f'))
plt.title('Properly Labeled Colorbar')
plt.show()
Best Practices and Accessibility
Use perceptually uniform colormaps by default. ‘viridis’, ‘plasma’, ‘cividis’, and ‘coolwarm’ maintain consistent brightness increments. Rainbow colormaps like ‘jet’ create artificial boundaries and fail in grayscale.
Match colormap type to data type. Sequential for ordered data, diverging for data with meaningful center points, qualitative for categories. This isn’t stylistic—it’s about accurate communication.
Test for colorblind accessibility. About 8% of men and 0.5% of women have color vision deficiencies. ‘viridis’ and ‘cividis’ work for most types. Avoid red-green combinations without additional visual cues.
Add colorbars with units. Your audience shouldn’t guess what colors mean.
Here’s a comparison showing these principles:
# Generate data with interesting structure
x = np.linspace(-2, 2, 100)
y = np.linspace(-2, 2, 100)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2)) * np.exp(-0.1 * (X**2 + Y**2))
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# Poor: Rainbow colormap
im1 = axes[0, 0].imshow(Z, cmap='jet', extent=[-2, 2, -2, 2])
axes[0, 0].set_title('❌ Poor: Rainbow (jet)')
plt.colorbar(im1, ax=axes[0, 0])
# Poor: Wrong type (qualitative for continuous)
im2 = axes[0, 1].imshow(Z, cmap='Set1', extent=[-2, 2, -2, 2])
axes[0, 1].set_title('❌ Poor: Qualitative for continuous data')
plt.colorbar(im2, ax=axes[0, 1])
# Good: Perceptually uniform
im3 = axes[1, 0].imshow(Z, cmap='viridis', extent=[-2, 2, -2, 2])
axes[1, 0].set_title('✓ Good: Perceptually uniform (viridis)')
cbar3 = plt.colorbar(im3, ax=axes[1, 0])
cbar3.set_label('Amplitude', rotation=270, labelpad=20)
# Good: Diverging for data centered at zero
im4 = axes[1, 1].imshow(Z, cmap='RdBu_r', extent=[-2, 2, -2, 2])
axes[1, 1].set_title('✓ Good: Diverging centered at zero')
cbar4 = plt.colorbar(im4, ax=axes[1, 1])
cbar4.set_label('Amplitude', rotation=270, labelpad=20)
plt.tight_layout()
plt.show()
The bottom line: colormaps are not decoration. They’re a fundamental part of how your visualization encodes information. Choose deliberately, test with your target audience, and always prioritize clarity over aesthetics. A boring, accessible plot that communicates clearly beats a beautiful plot that misleads every time.