Python: Asymmetrical colorbars#
Description
In Matplotlib it is very easy to use predefined colormaps, even if the data values are to be assigned asymmetrically. It becomes a little more complicated if, for example, you want to use a colormap with white in the middle, but not its first n-colors, i.e. ‘light blue/white/dark red’ instead of ‘dark blue/white/dark red’.
Software requirements
Python 3
numpy
matplotlib
Example script#
about_asymmetrical_colorbars.py
#!/usr/bin/env python
# coding: utf-8
# Asymmetrical colormaps
#
# In Matplotlib it is very easy to use predefined colormaps, even if the data
# values are to be assigned asymmetrically. It becomes a little more complicated
# if, for example, you want to use a colormap with white in the middle, but not
# its first n-colors, i.e. 'light blue/white/dark red' instead of
# 'dark blue/white/dark red'.
#
# This notebook contains some examples that provide solutions to those annoying
# problems of working with asymmetrical colormaps and colorbars.
#
# Content
# 1. Create random data
# 2. Example using vmin/vmax
# 3. Use normalization
# 4. Create colormap from RGBA arrays
# 5. Define colormap midpoint and cut off outlaying colors
#
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import matplotlib.colorbar as colorbar
import matplotlib.colors as mcolors
import matplotlib.cm as mcm
# Create random data
#-- set data minimum and maximum
vmin = -10.
vmax = 10.
#-- generate random data
n = 10
x = np.arange(1, n+1, 1)
y = np.arange(1, n+1, 1)
np.random.seed(20250101)
data = np.random.uniform(vmin, vmax, (n,n))
# Example using vmin/vmax
#
# First we start with the common use of the symmetrical colormap `RdBu` to plot
# random data. And draw next to it for comparison the plot generated using the
# vmin and vmax keywords to set the data range displayed.
#
# The left plot uses only the default settings for a symmetrical data range.
#
# The right plot uses the `vmin` and `vmax` keywords to limit the data range
# to -2 to 10.
fig, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8,3), layout='tight')
#-- left plot: use defaults
ax1.set_title('use defaults')
plot_l = ax1.pcolormesh(x, y, data, cmap='RdBu_r')
cb_left = plt.colorbar(plot_l, ax=ax1, shrink=0.9, pad=0.1)
#-- right plot: use vmin, vmax
ax2.set_title('vmin=-2, vmax=10.')
plot_r = ax2.pcolormesh(x, y, data, cmap='RdBu_r', vmin=-2., vmax=10.)
cb_right = plt.colorbar(plot_r, ax=ax2, shrink=0.9, pad=0.1)
plt.show()
# In the right plot the colors of the colorbar are still the same as in the left
# plot, only the colorbar labels and their mapping to the colors have changed -
# value zero is mapped to blue instead to white! This is not really what we want
# here and this is why we have to use another way in the next example to keep
# the white in the middle.
# Use normalization
#
# From https://matplotlib.org/stable/users/explain/colors/colormapnorms.html
# 'Objects that use colormaps by default linearly map the colors in the colormap
# from data values vmin to vmax'.
#
# Normalization: For asymmetrical colormaps you have to use the `TwoSlopeNorm`
# method instead of `Normalize` to be able to set the center of the colormap to
# a given value.
#
# The next example shows how to use `RdBu_r` which is the reversed orderd `RdBu`
# colormap and the `TwoSlopeNorm` normalization method to create an asymmetrical
# colorbar with white at value zero.
#
# The left plot shows the default colorbar that shows a nonlinear colorbar axis
# scaling.
#
# The plot on the right shows that we can change the non-linear representation
# of the colorbar to a linear representation by using the colorbar axis method
# `set_yscale`.
#-- create the plot
fig, (ax1,ax2) = plt.subplots(nrows=1, ncols=2, figsize=(8,3))
#-- for asymmetrical colormaps you have to use the `TwoSlopeNorm` method instead
# of `Normalize`
norm = mcolors.TwoSlopeNorm(vmin=-2., vcenter=0, vmax=vmax)
#-- use TwoSlopeNorm
ax1.set_title('TwoSlopeNorm')
plot_l = ax1.pcolormesh(x, y, data,
cmap='RdBu_r',
norm=norm)
cb_left = plt.colorbar(plot_l, ax=ax1, shrink=0.9, pad=0.1)
#-- and set cbar yscale to linear
ax2.set_title('TwoSlopeNorm, \ncbar yscale=linear')
plot_r = ax2.pcolormesh(x, y, data,
cmap='RdBu_r',
norm=norm)
cb_right = plt.colorbar(plot_r, ax=ax2, shrink=0.9, pad=0.1)
#-- colorbar: use a linear y-axis scale when working with an asymmetrical
#-- normalization
cb_right.ax.set_yscale('linear')
plt.show()
# Create colormap from RGBA arrays
#
# In the next example we define our own colormap from given blue and red RGBA
# arrays from the predefined colormaps `Blues` and `Reds`, stack the arrays,
# and use the `mcolors.LinearSegmentedColormap.from_list` method to generate a
# colormap.
#
# If the new colormap does not exist, we register it by a given name which
# allows us to use the new colormap name instead the object.
#-- RGBA colors
#-- Use some blues, reds and white in the middle.
#-- The whites (2 x) have to be in the middle of the colormap to use the
#-- TwoSlopeNorm method correctly.
whites = [[1.0, 1.0, 1.0, 1.0]] * 2
blues = mcm.Blues_r(np.linspace(0, 1, 127))
reds = mcm.Reds(np.linspace(0, 1, 127))
rgba_cmap = np.vstack((blues, whites, reds))
#-- create the colormap from the stacked RGBA array
cmap_name = 'BluesWhiteRed'
cmap = mcolors.LinearSegmentedColormap.from_list(cmap_name, rgba_cmap, N=len(rgba_cmap))
#-- register the new colormap for the later use by name
try:
mpl.colormaps.register(cmap, name=cmap_name)
except:
print(f'The colormap "{cmap_name}" is already registered')
print(f'New colormap "{cmap_name}" with n-colors: {cmap.N}')
# Create the plot
fig, ax = plt.subplots(figsize=(4,3))
#-- for asymmetrical colormaps you have to use the `TwoSlopeNorm` method instead
#-- of `Normalize`
norm = mcolors.TwoSlopeNorm(vmin=-2., vcenter=0, vmax=vmax)
plot = ax.pcolormesh(x, y, data,
cmap='BluesWhiteRed',
norm=norm)
cbar = plt.colorbar(plot, ax=ax, shrink=0.8, pad=0.07)
#-- colorbar: use a linear y-axis scale when working with an asymmetrical normalization
cbar.ax.set_yscale('linear')
plt.show()
# In the next example we don't want to use the dark blues and therefore we skip that colormap indices.
#-- RGBA colors
#-- Use some blues, reds and white in the middle.
#-- The whites (2 times) have to be in the middle of the colormap to use the
#-- TwoSlopeNorm method correctly.
whites = [[1.0, 1.0, 1.0, 1.0]] * 2
blues = mcm.Blues_r(np.linspace(0, 1, 256))
blues = blues[150:, :]
reds = mcm.Reds(np.linspace(0, 1, 256))
rgba_cmap = np.vstack((blues, whites, reds))
#-- create the colormap from the RGBA array
cmap_name = 'LightBlueWhiteRed'
cmap = mcolors.LinearSegmentedColormap.from_list(cmap_name, rgba_cmap, N=len(rgba_cmap))
#-- register the new colormap for the later use by name
try:
mpl.colormaps.register(cmap, name=cmap_name)
except:
print(f'The colormap "{cmap_name}" is already registered')
print(cmap.N)
fig, ax = plt.subplots(figsize=(4,3))
#-- for asymmetrical colormaps you have to use the `TwoSlopeNorm` method instead
#-- of `Normalize`
norm = mcolors.TwoSlopeNorm(vmin=-2., vcenter=0, vmax=10.)
plot = ax.pcolormesh(x, y, data,
cmap='LightBlueWhiteRed',
norm=norm)
cbar = plt.colorbar(plot, ax=ax, shrink=0.8, pad=0.07)
#-- colorbar: use a linear y-axis scale when working with an asymmetrical
#-- normalization
cbar.ax.set_yscale('linear')
plt.show()
# Unfortunately, you can now see that the result is not what we want. The zero
# point is shifted in the colorbar to light red, but should remain white.
#
# The next example shows a solution approach found in Stack Overflow.
# Define colormap midpoint and cut off outlaying colors
#
# The next example demonstrates the use of a pre-defined colormap with a user
# defined subclass of the `cm.Normalize` class to generate the plot and it's
# colorbar cutting off outlaying colors.
#
# Based on an example from _Stack Overflow_: <br>
# https://stackoverflow.com/questions/55665167/asymmetric-color-bar-with-fair-diverging-color-map <br>
# https://stackoverflow.com/questions/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib <br>
#-- generate random data and define the value range
vmin, vmax = -2., 10.
n = 10
x, y = np.meshgrid(np.arange(1, n+1, 1), np.arange(1, n+1, 1))
np.random.seed(20250101)
z = np.random.uniform(vmin, vmax, (n,n))
#-- subclassing cm.Normalize
class MidpointNormalize(mcolors.Normalize):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
self.midpoint = midpoint
mcolors.Normalize.__init__(self, vmin, vmax, clip)
def __call__(self, value, clip=None):
v_ext = np.max( [ np.abs(self.vmin), np.abs(self.vmax) ] )
x, y = [-v_ext, self.midpoint, v_ext], [0, 0.5, 1]
return np.ma.masked_array(np.interp(value, x, y))
norm = MidpointNormalize(midpoint=0)
#-- generate the plot
fig, ax = plt.subplots(figsize=(4.5, 4))
plot = ax.pcolormesh(x, y, z, cmap='RdBu_r', norm=norm)
cbar = plt.colorbar(plot)
plt.show()
plt.savefig(f'plot_asymmetrical_colorbars.png', bbox_inches='tight', dpi=150)