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:
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()
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()
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
Variable Assignment and Modification (In Python)
Why Does Python Assignment Not Return a Value
Database Does Not Update Automatically with MySQL and Python
I Don't Understand This Python _Del_ Behaviour
Displaying Subprocess Output to Stdout and Redirecting It
Permanent Fix for Opencv Videocapture
Replace Characters Not Working in Python
What Does % Do to Strings in Python
Pyqt: No Error Msg (Traceback) on Exit
Opencv Videocapture and Error: (-215:Assertion Failed) !_Src.Empty() in Function 'Cv::Cvtcolor'
How to Stop a Looping Thread in Python
Python 3.7 Anaconda Environment - Import _Ssl Dll Load Fail Error
How to Return a Value from _Init_ in Python