Defining the Midpoint of a Colormap in Matplotlib

Defining the midpoint of a colormap in matplotlib

Note that in matplotlib version 3.2+ the TwoSlopeNorm class was added. I think it covers your use case.
It can be used like this:

from matplotlib import colors
divnorm=colors.TwoSlopeNorm(vmin=-5., vcenter=0., vmax=10)
pcolormesh(your_data, cmap="coolwarm", norm=divnorm)

In matplotlib 3.1 the class was called DivergingNorm.

Matplotlib: Center colors in colorbar with diverging colormap using indexed color values

You can use the matplotlib built-in function that does the same thing:

matplotlib.colors.TwoSlopeNorm

See: https://matplotlib.org/3.2.2/gallery/userdemo/colormap_normalizations_diverging.html

matplotlib bwr-colormap, always centered on zero

Apparently, I found the answer myself after digging a little longer. pcolor offers the optional input vmin and vmax. If I set them to -1 and 1 respectively, it exactly solves the problem. The colorcoding then seems to be relative to vmin and vmax, not to the min and max of the data, which is plotted. So changing the plot command (and comments) to

# a plot ranging from -1 to 1, where the value 0 is colorcoded in white
ax = fig.add_subplot(1, 2, 1)
plt.pcolor(X, Y, Z, vmin=-1, vmax=1) # vmin, vmax not needed here
plt.colorbar()

# a plot ranging from -0.2 to 0.8, where the value 0 is colorcoded in white
ax = fig.add_subplot(1, 2, 2)
plt.pcolor(X, Y, Z*0.5 + 0.3, vmin=-1, vmax=1) # rescaled Z-Data
plt.colorbar()

It produces a figure as I need it:correct figure

So, setting vmin=-1, vmax=1 does the job, i do not have to change stuff on the colormap itself.

Python: Shifted logarithmic colorbar, white color offset to center

There are some questions and answers about defining a midpoint on a colorscale. Especially this one, which is also now part of the matplotlib documentation.

The idea is to subclass matplotlib.colors.Normalize and let it take a further argument midpoint. This can then be used to linearly interpolate the two ranges on either side of the midpoint to the ranges [0,0.5] and [0.5,1].

To have a midpoint on a logarithmic scale, we can in principle do the same thing, just that we subclass matplotlib.colors.LogNorm and take the logarithm of all values, then interpolate this logarithm on the ranges [0,0.5] and [0.5,1].

In the following example we have data between 0.001 and 10. Using the usual LogNorm this results in the middle of the colormap (white in the case of the RdBu colormap) to be at 0.1. If we want to have white at 1, we specify 1 as the midpoint in the MidPointLogNorm.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

x,y = np.meshgrid(np.linspace(-3,0,19), np.arange(10))
f = lambda x,y : 10**x*(1+y)
z = f(x,y)

fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(12,4.8))

im = ax.pcolormesh(x,y,z, cmap="RdBu_r", norm=LogNorm(vmin=z.min(), vmax=z.max()))
fig.colorbar(im, ax=ax)
ax.set_title("LogNorm")

class MidPointLogNorm(LogNorm):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
LogNorm.__init__(self,vmin=vmin, vmax=vmax, clip=clip)
self.midpoint=midpoint
def __call__(self, value, clip=None):
# I'm ignoring masked values and all kinds of edge cases to make a
# simple example...
x, y = [np.log(self.vmin), np.log(self.midpoint), np.log(self.vmax)], [0, 0.5, 1]
return np.ma.masked_array(np.interp(np.log(value), x, y))

im2 = ax2.pcolormesh(x,y,z, cmap="RdBu_r",
norm=MidPointLogNorm(vmin=z.min(), vmax=z.max(), midpoint=1))
fig.colorbar(im2, ax=ax2)
ax2.set_title("MidPointLogNorm")
plt.show()

Sample Image


Updated solution which works for nan values: You need to replace the nan values by some value (best one outside the range of values from the array) then mask the array by those numbers. Inside the MidPointLogNorm we need to take care of nan values, as shown in this question.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LogNorm

x,y = np.meshgrid(np.linspace(-3,0,19), np.arange(10))
f = lambda x,y : 10**x*(1+y)
z = f(x,y)
z[1:3,1:3] = np.NaN

#since nan values cannot be used on a log scale, we need to change them to
# something other than nan,
replace = np.nanmax(z)+900
z = np.where(np.isnan(z), replace, z)
# now we can mask the array
z = np.ma.masked_where(z == replace, z)

fig, (ax,ax2) = plt.subplots(ncols=2, figsize=(12,4.8))

im = ax.pcolormesh(x,y,z, cmap="RdBu_r", norm=LogNorm(vmin=z.min(), vmax=z.max()))
fig.colorbar(im, ax=ax)
ax.set_title("LogNorm")

class MidPointLogNorm(LogNorm):
def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False):
LogNorm.__init__(self,vmin=vmin, vmax=vmax, clip=clip)
self.midpoint=midpoint
def __call__(self, value, clip=None):
result, is_scalar = self.process_value(value)
x, y = [np.log(self.vmin), np.log(self.midpoint), np.log(self.vmax)], [0, 0.5, 1]
return np.ma.array(np.interp(np.log(value), x, y), mask=result.mask, copy=False)

im2 = ax2.pcolormesh(x,y,z, cmap="RdBu_r",
norm=MidPointLogNorm(vmin=z.min(), vmax=z.max(), midpoint=1))
fig.colorbar(im2, ax=ax2)
ax2.set_title("MidPointLogNorm")
plt.show()

Midpoint of Color Palette

ImportanceOfBeingErnest correctly pointed out that my first comment wasn't entirely clear (or accurately worded)..

Most plotting functions in mpl have a kwarg: norm= this denotes a class (subclass of mpl.colors.Normalize) that will map your array of data to the values [0 - 1] for the purpose of mapping to the colormap, but not actually impact the numerical values of the data. There are several built in subclasses, and you can also create your own. For this application, I would probably utilize BoundaryNorm. This class maps N-1 evenly spaced colors to the space between N discreet boundaries.

I have modified the example slightly to better fit your application:

#adaptation of https://matplotlib.org/users/colormapnorms.html#discrete-bounds

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.colors as colors
from matplotlib.mlab import bivariate_normal

#example data
N = 100
X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)]
Z1 = (bivariate_normal(X, Y, 1., 1., 1.0, 1.0))**2 \
- 0.4 * (bivariate_normal(X, Y, 1.0, 1.0, -1.0, 0.0))**2
Z1 = Z1/0.03

'''
BoundaryNorm: For this one you provide the boundaries for your colors,
and the Norm puts the first color in between the first pair, the
second color between the second pair, etc.
'''

fig, ax = plt.subplots(3, 1, figsize=(8, 8))
ax = ax.flatten()
# even bounds gives a contour-like effect
bounds = np.linspace(-1, 1)
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)
pcm = ax[0].pcolormesh(X, Y, Z1,
norm=norm,
cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[0], extend='both', orientation='vertical')

# clipped bounds emphasize particular region of data:
bounds = np.linspace(-.2, .5)
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)
pcm = ax[1].pcolormesh(X, Y, Z1, norm=norm, cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical')

# now if we want 0 to be white still, we must have 0 in the middle of our array
bounds = np.append(np.linspace(-.2, 0), np.linspace(0, .5))
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)
pcm = ax[2].pcolormesh(X, Y, Z1, norm=norm, cmap='RdBu_r')
fig.colorbar(pcm, ax=ax[2], extend='both', orientation='vertical')

fig.show()

Sample Image

Matplotlib Symmetric Logarithmic colormap not centered at zero

Here's my fix - it essentially uses a manual linear scale for the regions close to the center, and uses a logarithmic scale for the regions further outside. You specify lin_thres and the other arguments just as you would for SymLogNorm.

class MidpointLogNorm(colors.SymLogNorm):
"""
Normalise the colorbar so that diverging bars work there way either side from a prescribed midpoint value)
e.g. im=ax1.imshow(array, norm=MidpointNormalize(midpoint=0.,vmin=-100, vmax=100))

All arguments are the same as SymLogNorm, except for midpoint
"""
def __init__(self, lin_thres, lin_scale, midpoint=None, vmin=None, vmax=None):
self.midpoint = midpoint
self.lin_thres = lin_thres
self.lin_scale = lin_scale
#fraction of the cmap that the linear component occupies
self.linear_proportion = (lin_scale / (lin_scale + 1)) * 0.5
print(self.linear_proportion)

colors.SymLogNorm.__init__(self, lin_thres, lin_scale, vmin, vmax)

def __get_value__(self, v, log_val, clip=None):
if v < -self.lin_thres or v > self.lin_thres:
return log_val

x = [-self.lin_thres, self.midpoint, self.lin_thres]
y = [0.5 - self.linear_proportion, 0.5, 0.5 + self.linear_proportion]
interpol = np.interp(v, x, y)
return interpol

def __call__(self, value, clip=None):
log_val = colors.SymLogNorm.__call__(self, value)

out = [0] * len(value)
for i, v in enumerate(value):
out[i] = self.__get_value__(v, log_val[i])

return np.ma.masked_array(out)

I drew inspiration centering around the midpoint from here: http://chris35wills.github.io/matplotlib_diverging_colorbar/



Related Topics



Leave a reply



Submit