# Defining the midpoint of a colormap in matplotlib

Posted on

### Question :

Defining the midpoint of a colormap in matplotlib

I want to set the middle point of a colormap, ie my data goes from -5 to 10, i want zero to be the middle. I think the way to do it is subclassing normalize and using the norm, but i didn’t find any example and it is not clear to me, what exactly i have to implement.

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

``````from matplotlib import colors
colors.DivergingNorm(vmin=-4000., vcenter=0., vmax=10000)
``````

In matplotlib 3.2 the class has been renamed to TwoSlopesNorm

I know this is late to the game, but I just went through this process and came up with a solution that perhaps less robust than subclassing normalize, but much simpler. I thought it’d be good to share it here for posterity.

### The function

``````import numpy as np
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import AxesGrid

def shiftedColorMap(cmap, start=0, midpoint=0.5, stop=1.0, name='shiftedcmap'):
'''
Function to offset the "center" of a colormap. Useful for
data with a negative min and positive max and you want the
middle of the colormap's dynamic range to be at zero.

Input
-----
cmap : The matplotlib colormap to be altered
start : Offset from lowest point in the colormap's range.
Defaults to 0.0 (no lower offset). Should be between
0.0 and `midpoint`.
midpoint : The new center of the colormap. Defaults to
0.5 (no shift). Should be between 0.0 and 1.0. In
general, this should be  1 - vmax / (vmax + abs(vmin))
For example if your data range from -15.0 to +5.0 and
you want the center of the colormap at 0.0, `midpoint`
should be set to  1 - 5/(5 + 15)) or 0.75
stop : Offset from highest point in the colormap's range.
Defaults to 1.0 (no upper offset). Should be between
`midpoint` and 1.0.
'''
cdict = {
'red': [],
'green': [],
'blue': [],
'alpha': []
}

# regular index to compute the colors
reg_index = np.linspace(start, stop, 257)

# shifted index to match the data
shift_index = np.hstack([
np.linspace(0.0, midpoint, 128, endpoint=False),
np.linspace(midpoint, 1.0, 129, endpoint=True)
])

for ri, si in zip(reg_index, shift_index):
r, g, b, a = cmap(ri)

cdict['red'].append((si, r, r))
cdict['green'].append((si, g, g))
cdict['blue'].append((si, b, b))
cdict['alpha'].append((si, a, a))

newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
plt.register_cmap(cmap=newcmap)

return newcmap
``````

### An example

``````biased_data = np.random.random_integers(low=-15, high=5, size=(37,37))

orig_cmap = matplotlib.cm.coolwarm
shifted_cmap = shiftedColorMap(orig_cmap, midpoint=0.75, name='shifted')
shrunk_cmap = shiftedColorMap(orig_cmap, start=0.15, midpoint=0.75, stop=0.85, name='shrunk')

fig = plt.figure(figsize=(6,6))
grid = AxesGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.5,
label_mode="1", share_all=True,
cbar_location="right", cbar_mode="each",

# normal cmap
im0 = grid.imshow(biased_data, interpolation="none", cmap=orig_cmap)
grid.cbar_axes.colorbar(im0)
grid.set_title('Default behavior (hard to see bias)', fontsize=8)

im1 = grid.imshow(biased_data, interpolation="none", cmap=orig_cmap, vmax=15, vmin=-15)
grid.cbar_axes.colorbar(im1)
grid.set_title('Centered zero manually,nbut lost upper end of dynamic range', fontsize=8)

im2 = grid.imshow(biased_data, interpolation="none", cmap=shifted_cmap)
grid.cbar_axes.colorbar(im2)
grid.set_title('Recentered cmap with function', fontsize=8)

im3 = grid.imshow(biased_data, interpolation="none", cmap=shrunk_cmap)
grid.cbar_axes.colorbar(im3)
grid.set_title('Recentered cmap with functionnand shrunk range', fontsize=8)

for ax in grid:
ax.set_yticks([])
ax.set_xticks([])
``````

### Results of the example: Here is a solution subclassing Normalize. To use it

``````norm = MidPointNorm(midpoint=3)
imshow(X, norm=norm)
``````

Here is the Class:

``````import numpy as np
from numpy import ma
from matplotlib import cbook
from matplotlib.colors import Normalize

class MidPointNorm(Normalize):
def __init__(self, midpoint=0, vmin=None, vmax=None, clip=False):
Normalize.__init__(self,vmin, vmax, clip)
self.midpoint = midpoint

def __call__(self, value, clip=None):
if clip is None:
clip = self.clip

result, is_scalar = self.process_value(value)

self.autoscale_None(result)
vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

if not (vmin < midpoint < vmax):
raise ValueError("midpoint must be between maxvalue and minvalue.")
elif vmin == vmax:
result.fill(0) # Or should it be all masked? Or 0.5?
elif vmin > vmax:
raise ValueError("maxvalue must be bigger than minvalue")
else:
vmin = float(vmin)
vmax = float(vmax)
if clip:
result = ma.array(np.clip(result.filled(vmax), vmin, vmax),

# ma division is very slow; we can take a shortcut
resdat = result.data

#First scale to -1 to 1 range, than to from 0 to 1.
resdat -= midpoint
resdat[resdat>0] /= abs(vmax - midpoint)
resdat[resdat<0] /= abs(vmin - midpoint)

resdat /= 2.
resdat += 0.5

if is_scalar:
result = result
return result

def inverse(self, value):
if not self.scaled():
raise ValueError("Not invertible until scaled")
vmin, vmax, midpoint = self.vmin, self.vmax, self.midpoint

if cbook.iterable(value):
val = ma.asarray(value)
val = 2 * (val-0.5)
val[val>0]  *= abs(vmax - midpoint)
val[val<0] *= abs(vmin - midpoint)
val += midpoint
return val
else:
val = 2 * (value - 0.5)
if val < 0:
return  val*abs(vmin-midpoint) + midpoint
else:
return  val*abs(vmax-midpoint) + midpoint
``````

It’s easiest to just use the `vmin` and `vmax` arguments to `imshow` (assuming you’re working with image data) rather than subclassing `matplotlib.colors.Normalize`.

E.g.

``````import numpy as np
import matplotlib.pyplot as plt

data = np.random.random((10,10))
# Make the data range from about -5 to 10
data = 10 / 0.75 * (data - 0.25)

plt.imshow(data, vmin=-10, vmax=10)
plt.colorbar()

plt.show()
`````` Here I create a subclass of `Normalize` followed by a minimal example.

``````import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt

class MidpointNormalize(mpl.colors.Normalize):
def __init__(self, vmin, vmax, midpoint=0, clip=False):
self.midpoint = midpoint
mpl.colors.Normalize.__init__(self, vmin, vmax, clip)

def __call__(self, value, clip=None):
normalized_min = max(0, 1 / 2 * (1 - abs((self.midpoint - self.vmin) / (self.midpoint - self.vmax))))
normalized_max = min(1, 1 / 2 * (1 + abs((self.vmax - self.midpoint) / (self.midpoint - self.vmin))))
normalized_mid = 0.5
x, y = [self.vmin, self.midpoint, self.vmax], [normalized_min, normalized_mid, normalized_max]
return np.ma.masked_array(np.interp(value, x, y))

vals = np.array([[-5., 0], [5, 10]])
vmin = vals.min()
vmax = vals.max()

norm = MidpointNormalize(vmin=vmin, vmax=vmax, midpoint=0)
cmap = 'RdBu_r'

plt.imshow(vals, cmap=cmap, norm=norm)
plt.colorbar()
plt.show()
``````

The same example with only positive data `vals = np.array([[1., 3], [6, 10]])` Properties:

• The midpoint gets the middle color.
• Upper and lower ranges are rescaled by the same linear transformation.
• Only the color which appear on the picture are shown in the colorbar.
• Seems to work fine even if `vmin` is bigger than `midpoint` (did not test all the edge cases though).

This solution is inspired by a class with the same name from this page

Not sure if you are still looking for an answer. For me, trying to subclass `Normalize` was unsuccessful. So I focused on manually creating a new data set, ticks and tick-labels to get the effect I think you are aiming for.

I found the `scale` module in matplotlib that has a class used to transform line plots by the ‘syslog’ rules, so I use that to transform the data. Then I scale the data so that it goes from 0 to 1 (what `Normalize` usually does), but I scale the positive numbers differently from the negative numbers. This is because your vmax and vmin might not be the same, so .5 -> 1 might cover a larger positive range than .5 -> 0, the negative range does. It was easier for me to create a routine to calculate the tick and label values.

Below is the code and an example figure.

``````import numpy as np
import matplotlib.pyplot as plt
import matplotlib.mpl as mpl
import matplotlib.scale as scale

NDATA = 50
VMAX=10
VMIN=-5
LINTHRESH=1e-4

def makeTickLables(vmin,vmax,linthresh):
"""
make two lists, one for the tick positions, and one for the labels
at those positions. The number and placement of positive labels is
different from the negative labels.
"""
nvpos = int(np.log10(vmax))-int(np.log10(linthresh))
nvneg = int(np.log10(np.abs(vmin)))-int(np.log10(linthresh))+1
ticks = []
labels = []
lavmin = (np.log10(np.abs(vmin)))
lvmax = (np.log10(np.abs(vmax)))
llinthres = int(np.log10(linthresh))
# f(x) = mx+b
# f(llinthres) = .5
# f(lavmin) = 0
m = .5/float(llinthres-lavmin)
b = (.5-llinthres*m-lavmin*m)/2
for itick in range(nvneg):
labels.append(-1*float(pow(10,itick+llinthres)))
ticks.append((b+(itick+llinthres)*m))
# add vmin tick
labels.append(vmin)
ticks.append(b+(lavmin)*m)

# f(x) = mx+b
# f(llinthres) = .5
# f(lvmax) = 1
m = .5/float(lvmax-llinthres)
b = m*(lvmax-2*llinthres)
for itick in range(1,nvpos):
labels.append(float(pow(10,itick+llinthres)))
ticks.append((b+(itick+llinthres)*m))
# add vmax tick
labels.append(vmax)
ticks.append(b+(lvmax)*m)

return ticks,labels

data = (VMAX-VMIN)*np.random.random((NDATA,NDATA))+VMIN

# define a scaler object that can transform to 'symlog'
scaler = scale.SymmetricalLogScale.SymmetricalLogTransform(10,LINTHRESH)
datas = scaler.transform(data)

# scale datas so that 0 is at .5
# so two seperate scales, one for positive and one for negative
data2 = np.where(np.greater(data,0),
.75+.25*datas/np.log10(VMAX),
.25+.25*(datas)/np.log10(np.abs(VMIN))
)

ticks,labels=makeTickLables(VMIN,VMAX,LINTHRESH)

cmap = mpl.cm.jet
fig = plt.figure()
im = ax.imshow(data2,cmap=cmap,vmin=0,vmax=1)
cbar = plt.colorbar(im,ticks=ticks)
cbar.ax.set_yticklabels(labels)

fig.savefig('twoscales.png')
`````` Feel free to adjust the “constants” (eg `VMAX`) at the top of the script to confirm that it behaves well.

I was using the excellent answer from Paul H, but ran into an issue because some of my data ranged from negative to positive, while other sets ranged from 0 to positive or from negative to 0; in either case I wanted 0 to be coloured as white (the midpoint of the colormap I’m using). With the existing implementation, if your `midpoint` value is equal to 1 or 0, the original mappings were not being overwritten. You can see that in the following picture: The 3rd column looks correct, but the dark blue area in the 2nd column and the dark red area in the remaining columns are all supposed to be white (their data values are in fact 0). Using my fix gives me: My function is essentially the same as that from Paul H, with my edits at the start of the `for` loop:

``````def shiftedColorMap(cmap, min_val, max_val, name):
'''Function to offset the "center" of a colormap. Useful for data with a negative min and positive max and you want the middle of the colormap's dynamic range to be at zero. Adapted from https://stackoverflow.com/questions/7404116/defining-the-midpoint-of-a-colormap-in-matplotlib

Input
-----
cmap : The matplotlib colormap to be altered.
start : Offset from lowest point in the colormap's range.
Defaults to 0.0 (no lower ofset). Should be between
0.0 and `midpoint`.
midpoint : The new center of the colormap. Defaults to
0.5 (no shift). Should be between 0.0 and 1.0. In
general, this should be  1 - vmax/(vmax + abs(vmin))
For example if your data range from -15.0 to +5.0 and
you want the center of the colormap at 0.0, `midpoint`
should be set to  1 - 5/(5 + 15)) or 0.75
stop : Offset from highets point in the colormap's range.
Defaults to 1.0 (no upper ofset). Should be between
`midpoint` and 1.0.'''
epsilon = 0.001
start, stop = 0.0, 1.0
min_val, max_val = min(0.0, min_val), max(0.0, max_val) # Edit #2
midpoint = 1.0 - max_val/(max_val + abs(min_val))
cdict = {'red': [], 'green': [], 'blue': [], 'alpha': []}
# regular index to compute the colors
reg_index = np.linspace(start, stop, 257)
# shifted index to match the data
shift_index = np.hstack([np.linspace(0.0, midpoint, 128, endpoint=False), np.linspace(midpoint, 1.0, 129, endpoint=True)])
for ri, si in zip(reg_index, shift_index):
if abs(si - midpoint) < epsilon:
r, g, b, a = cmap(0.5) # 0.5 = original midpoint.
else:
r, g, b, a = cmap(ri)
cdict['red'].append((si, r, r))
cdict['green'].append((si, g, g))
cdict['blue'].append((si, b, b))
cdict['alpha'].append((si, a, a))
newcmap = matplotlib.colors.LinearSegmentedColormap(name, cdict)
plt.register_cmap(cmap=newcmap)
return newcmap
``````

EDIT: I ran into a similar issue yet again when some of my data ranged from a small positive value to a larger positive value, where the very low values were being coloured red instead of white. I fixed it by adding line `Edit #2` in the code above.

If you don’t mind working out the ratio between vmin, vmax, and zero, this is a pretty basic linear map from blue to white to red, that sets white according to the ratio `z`:

``````def colormap(z):
"""custom colourmap for map plots"""

cdict1 = {'red': ((0.0, 0.0, 0.0),
(z,   1.0, 1.0),
(1.0, 1.0, 1.0)),
'green': ((0.0, 0.0, 0.0),
(z,   1.0, 1.0),
(1.0, 0.0, 0.0)),
'blue': ((0.0, 1.0, 1.0),
(z,   1.0, 1.0),
(1.0, 0.0, 0.0))
}

return LinearSegmentedColormap('BlueRed1', cdict1)
``````

The cdict format is fairly simple: the rows are points in the gradient that gets created: the first entry is the x-value (the ratio along the gradient from 0 to 1), the second is the end value for the previous segment, and the third is the start value for the next segment – if you want smooth gradients, the latter two are always the same. See the docs for more detail.