The other answer makes it a little more complicated than it needs to be. In order to have the middle point of the colormap at 0, use a DivergingNorm with vcenter=0.

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

x, y = np.meshgrid(np.linspace(0,50,51), np.linspace(0,50,51))
z = np.linspace(-2,4,50*50).reshape(50,50)

norm = DivergingNorm(vmin=z.min(), vcenter=0, vmax=z.max())
pc = plt.pcolormesh(x,y,z, norm=norm, cmap="RdBu_r")
plt.colorbar(pc)

plt.show()

Note: From matplotlib 3.2 onwards DivergingNorm will be renamed to TwoSlopeNorm

Answer from ImportanceOfBeingErnest on Stack Overflow
Top answer
1 of 2
12

The other answer makes it a little more complicated than it needs to be. In order to have the middle point of the colormap at 0, use a DivergingNorm with vcenter=0.

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

x, y = np.meshgrid(np.linspace(0,50,51), np.linspace(0,50,51))
z = np.linspace(-2,4,50*50).reshape(50,50)

norm = DivergingNorm(vmin=z.min(), vcenter=0, vmax=z.max())
pc = plt.pcolormesh(x,y,z, norm=norm, cmap="RdBu_r")
plt.colorbar(pc)

plt.show()

Note: From matplotlib 3.2 onwards DivergingNorm will be renamed to TwoSlopeNorm

2 of 2
2

In order to get the desired norm, set the same number of colors on the negative as on the positive side.

Alternatively, you could use the unmodified norm, and create a special colormap. Such a colormap would have 1/3rd of blue-to-white colors and 2/3rd white-to-red colors. A benefit would be that the colorbar looks nicer. Such an approach only works if the balance between negative and positive numbers isn't too extreme.

Here is demo code with generated data. zz is chosen to be a sine rotated around the center, and scaled to go from -2 to 4, so symmetric around 1. At the left the image is shown with the modified colormap. At the right, the norm is changed to force white at zero.

Because of the red coloring of all positive values, the red bands are wider than the blue. In an image without changing norms nor colormaps, the bands would have equal width. The colorbars indicate the zero to be white.

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

x = np.linspace(-20, 20, 500)
y = np.linspace(-20, 20, 500)
xx, yy = np.meshgrid(x, y)
zz = np.sin(np.sqrt(xx * xx + yy * yy)) * 3 + 1

negatives = -2.0
positives = 4.0

bounds_min = np.linspace(negatives, 0, 129)
bounds_max = np.linspace(0, positives, 129)[1:]
    # the zero is only needed once
    # in total there will be 257 bounds, so 256 bins
bounds = np.concatenate((bounds_min, bounds_max), axis=None)
norm = colors.BoundaryNorm(boundaries=bounds, ncolors=256)

num_neg_colors = int(256 / (positives - negatives) * (-negatives))
num_pos_colors = 256 - num_neg_colors
cmap_BuRd = plt.cm.RdBu_r
colors_2neg_4pos = [cmap_BuRd(0.5*c/num_neg_colors) for c in range(num_neg_colors)] +\
                   [cmap_BuRd(1-0.5*c/num_pos_colors) for c in range(num_pos_colors)][::-1]
cmap_2neg_4pos = colors.LinearSegmentedColormap.from_list('cmap_2neg_4pos', colors_2neg_4pos, N=256)

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))

mesh1 = ax1.pcolormesh(xx, yy, zz, cmap=cmap_2neg_4pos)
ax1.set_aspect('equal')
ax1.set_title('using a modified cmap')
fig.colorbar(mesh1, ax=ax1)

mesh2 = ax2.pcolormesh(xx, yy, zz, norm=norm, cmap='RdBu_r')
ax2.set_aspect('equal')
ax2.set_title('using a special norm')
ticks = np.append(np.arange(-2.0, 0, 0.25), np.arange(0, 4.001, 0.5))
fig.colorbar(mesh2, ax=ax2, ticks=ticks)

plt.show()

Following code plots the norm, which looks like a step function. Only with 257 bounds this step function has the correct shape everywhere (zooming to x at -2, 0 and 4).

nx = np.linspace(-3,5,10000)
plt.plot(nx, norm(nx))

PS: There is an alternative method to create a similar colormap. But trying it out, it is clear that the RdBu colormap is fine-tuned and produces much better looking plots.

norm_2neg_4pos = mcolors.Normalize(negatives, positives)
colors_2neg_4pos = [[0, 'blue'],
                    [norm_2neg_4pos(0.0), "white"],
                    [1, 'red']]
cmap_2neg_4pos = mcolors.LinearSegmentedColormap.from_list("", colors_2neg_4pos)

Still another simple solution is rescaling everything between -4 and 4. However, this would lose the darker blues. An alternative to 'RdBu_r' is 'seismic' with a different way to run from red over white to blue.

ax.pcolormesh(xx, yy, zz, vmin=-positives, vmax=positives, cmap='RdBu_r')
Top answer
1 of 3
18

You can use matplotlib.colors.TwoSlopeNorm like this:

# define your scale, with white at zero
vmin = -0.2 
vmax = 0.8
norm = colors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)

In your example would be,

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

# some arbitrary data to plot
x = numpy.linspace(0, 2*numpy.pi, 30)
y = numpy.linspace(0, 2*numpy.pi, 20)
[X, Y] = numpy.meshgrid(x, y)
Z = numpy.sin(X)*numpy.cos(Y)

fig = plt.figure()
plt.ion()
plt.set_cmap('bwr') # a good start: blue to white to red colormap

# a plot ranging from -1 to 1, hence the value 0 (the average) is colorcoded in white
ax = fig.add_subplot(1, 2, 1)
plt.pcolor(X, Y, Z)
plt.colorbar()

# a plot ranging from -0.2 to 0.8 hence 0.3 (the average) is colorcoded in white
ax = fig.add_subplot(1, 2, 2)

# define your scale, with white at zero
vmin = -0.2 
vmax = 0.8
norm = colors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)

plt.pcolor(X, Y, Z, vmin=vmin, vmax=vmax, norm=norm)   
plt.colorbar()

will give you:

2 of 3
7

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.

🌐
GitHub
github.com › mne-tools › mne-python › issues › 5693
Diverging colormaps for scalp plots: Always have "white" as zero · Issue #5693 · mne-tools/mne-python
November 4, 2018 - One solution to get the full color range and still the zero value centered on white color is described here (also provides a minimal working example to play around with): http://chris35wills.github.io/matplotlib_diverging_colorbar/ Basically, it is possible by subclassing mpl.color.Normalize · # set the colormap and centre the colorbar class MidpointNormalize(mpl.colors.Normalize): """Normalise the colorbar.""" def __init__(self, vmin=None, vmax=None, midpoint=None, clip=False): self.midpoint = midpoint mpl.colors.Normalize.__init__(self, vmin, vmax, clip) def __call__(self, value, clip=None): x, y = [self.vmin, self.midpoint, self.vmax], [0, 0.5, 1] return np.ma.masked_array(np.interp(value, x, y), np.isnan(value)) ...
Author: mne-tools
🌐
Matplotlib
matplotlib.org › stable › tutorials › colors › colormapnorms.html
Matplotlib
August 13, 2021 - Matplotlib has support for visualizing information with a wide array of colors and colormaps.
🌐
Matplotlib
matplotlib.org › stable › users › explain › colors › colormapnorms.html
Colormap normalization — Matplotlib 3.11.2 documentation
will map the data in Z linearly from -1 to +1, so Z=0 will give a color at the center of the colormap RdBu_r (white in this case).
🌐
Matplotlib
matplotlib.org › stable › users › explain › colors › colormaps.html
Choosing Colormaps in Matplotlib — Matplotlib 3.11.2 documentation
Some of the Sequential2 colormaps have decent enough grayscale representations, though some (autumn, spring, summer, winter) have very little grayscale change. If a colormap like this was used in a plot and then the plot was printed to grayscale, a lot of the information may map to the same gray values. The Diverging colormaps mostly vary from darker gray on the outer edges to white in the middle.
🌐
Notebook Community
notebook.community › PyHOGS › pyhogs.github.io › pyhogs-code › notebooks › examples › Colormap_examples
Color and colormap basics
With diverging colormaps, you're often trying to show values that positively or negatively diverge from a central tendency or zero value, and, therefore it is important to make sure that the desired zero-color (usually white, light gray, or yellowish) is displayed correctly. First, and most importantly, is to set the colorlimits, variously called vmin/vmax or clim.
Find elsewhere
Top answer
1 of 1
4

For your problem, I suggest setting the first level to be greater than zero, and using the extend feature of colorbar to specify zero x, y locations.

First we import libs and try to generate some random data from numpy's normal distribution function on a sparse grid.

from copy import copy

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

# generate sample data
x0, x1 = -5, 5
y0, y1 = -3, 3
x = np.linspace(x0, x1, 60)
y = np.linspace(y0, y1, 60)
X, Y = np.meshgrid(x, y)
Z = np.random.randn(60, 60)

To illustrate your problem, we set all negative values to zero, rendering non-negative grid data Z.

Z = np.where(Z > 0, Z, 0)

Now comes to the key point! Simply add a out-of-range color to a colormap.

palette = copy(plt.get_cmap('viridis_r'))
palette.set_under('white', 1.0)  # 1.0 represents not transparent

You may also use set_over, set_bad methods to set color for over-range data and undefined (NaN) data.

Set subplots:

fig, ax = plt.subplots(1, 1)

Set levels for plot, but do remember to modify the lowest level to a positive number close to zero.

levels = np.arange(0, 3.5, 0.5)
levels[0] = 1e-5

Here's how to plot the array Z.

norm = colors.BoundaryNorm(levels, ncolors=palette.N)
im = ax.imshow(Z, cmap=palette,
               norm=norm,
               aspect='auto', extent=[x0, x1, y0, y1])
# Possible extend options include: ['min', 'max', 'both', 'neither']
cbar = fig.colorbar(im, extend='min', shrink=0.9, ax=ax)
plt.savefig('draw.png', dpi=300)
# or
# plt.show()

Displaying the lowest level as 0.0 may lead to some ambiguity, perhaps setting levels[0] to 0.1 look better to you.

You may refer to following links for more:

https://matplotlib.org/examples/pylab_examples/image_masked.html

https://matplotlib.org/examples/pylab_examples/contourf_demo.html

🌐
Matplotlib
matplotlib.org › stable › users › explain › colors › colormap-manipulation.html
Creating Colormaps in Matplotlib — Matplotlib 3.11.2 documentation
There are also external libraries like palettable that have many extra colormaps. However, we may also want to create or manipulate our own colormaps. This can be done using the class ListedColormap or LinearSegmentedColormap. Both colormap classes map values between 0 and 1 to colors.
🌐
Mines-paristech
members.cbio.mines-paristech.fr › ~nvaroquaux › tmp › matplotlib › users › colormapnorms.html
Colormap Normalization — Matplotlib 2.0.0b4.post2415.dev0+g315f2c9 documentation
will map the data in Z linearly from -1 to +1, so Z=0 will give a color at the center of the colormap RdBu_r (white in this case). Matplotlib does this mapping in two steps, with a normalization from [0,1] occurring first, and then mapping onto the indices in the colormap.
Top answer
1 of 11
101

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",
                cbar_size="7%", cbar_pad="2%")

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

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

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

im3 = grid[3].imshow(biased_data, interpolation="none", cmap=shrunk_cmap)
grid.cbar_axes[3].colorbar(im3)
grid[3].set_title('Recentered cmap with function\nand shrunk range', fontsize=8)

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

Results of the example:

2 of 11
61

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.

Top answer
1 of 1
12

Rather than using a stock cmap, I will walk thorough the production of your own.

As you have already spotted, in order to have absolute control of the colors (without passing a colors array) when using cmaps, the number of colors in a cmap should be equal to the number of levels - 1.

We can easily demonstrate this with the following example. Lets make a colormap with the three primary colors in:

import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy


data = numpy.arange(12).reshape(3, 4)

cmap = mcolors.ListedColormap([(0, 0, 1), 
                               (0, 1, 0), 
                               (1, 0, 0)])

However, when we come to use this with anything other than 3+1 levels, we will get results that you may not expect:

plt.subplot(121)
plt.contourf(data, cmap=cmap, levels=[1, 4, 8, 10])

plt.subplot(122)
plt.contourf(data, cmap=cmap, levels=[1, 4, 8])

plt.show()

So I have shown that your levels must be of length n_colors-1, but because we want to have 3 colors, and the middle color always shown, we must also have an even number of levels.

Good, now we have some ground rules, lets make a LinearSegmentedColormap which is essentially just a ListedColormap with interpolation to N colors:

levs = range(12)
assert len(levs) % 2 == 0, 'N levels must be even.'

cmap = mcolors.LinearSegmentedColormap.from_list(name='red_white_blue', 
                                                 colors =[(0, 0, 1), 
                                                          (1, 1., 1), 
                                                          (1, 0, 0)],
                                                 N=len(levs)-1,
                                                 )
plt.contourf(data, cmap=cmap, levels=levs)
plt.show()

We can verify that the normailzed cmap has white as the middle color programatically:

>>> print cmap(0.5)
(1.0, 1.0, 1.0, 1.0)

Hopefully this will give you all the information you need to be able to make any colormap you like and to use it successfully in contouring.

Top answer
1 of 1
1

As has been said in the comments you need to round the zero point value to a multiple of 1/N if N is the number of colors in the colormap. Of course the difference between the zero point value and any other colors in the colormap must be bigger than 1/N.

So here is a solution

import matplotlib.colors as col
from matplotlib.colors import LinearSegmentedColormap
import numpy as np 

N=256
zero_pt0 = 0.98181818181818181
zero_pt_rounded = np.ceil(zero_pt0*(N-1))/float(N-1)
print (zero_pt_rounded)

cmap = LinearSegmentedColormap.from_list(
    'mycmap', [(0, 'blue'), (zero_pt_rounded, 'white'), (1, 'red')], N=N)

# verify if original point zero_pt0 gives white color
rgb = cmap(zero_pt0)[:3]
print(col.rgb2hex(rgb))

Concerning your older question: It's not clear what the line zero_pt = 1. - H.max() / (H.max() - H.min()) should do. I would guess that you need to put

data_value_for_white color = ...
zero_pt = (data_value_for_white color - H.min()) / (H.max() - H.min())

Furthermore, in order to make sure the desired value is covered by the white color in the colormap, we would need to make the two adjacent colors in the colormap white.

N = 256 # number of colors in the colormap
zero_pt0 = np.floor(zero_pt*(N-1))/float(N-1)
zero_pt1 = np.ceil(zero_pt*(N-1))/float(N-1)
cmap = LinearSegmentedColormap.from_list('mycmap',
         [(0, 'blue'), (zero_pt0, 'white'),(zero_pt1, 'white'), (1, 'red')], N=N)

A complete solution would therefore look like:

import pickle
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import LinearSegmentedColormap, rgb2hex


with open('data.pkl', 'r') as f:
    H = pickle.load(f)
with open('edges.pkl', 'r') as f:
    edges = pickle.load(f)

# Define zero point for empty bins which should be colored in white.
data_value_for_white_color = 0.
zero_pt = (data_value_for_white_color - H.min()) / float(H.max() - H.min())

N=256
zero_pt0 = np.floor(zero_pt*(N-1))/float(N-1)
zero_pt1 = np.ceil(zero_pt*(N-1))/float(N-1)

cmap = LinearSegmentedColormap.from_list('mycmap',
     [(0, 'blue'), (zero_pt0, 'white'),(zero_pt1, 'white'), (1, 'red')], N=N)

Y, X = np.meshgrid(edges[0], edges[1])
plt.pcolormesh(X, Y, H, cmap=cmap, vmin=H.min(), vmax=H.max())
plt.show()

🌐
Matplotlib
matplotlib.org › stable › tutorials › colors › colormap-manipulation.html
Creating Colormaps in Matplotlib — Matplotlib 3.7.2 documentation
May 1, 2020 - First, getting a named colormap, most of which are listed in Choosing Colormaps in Matplotlib, may be done using matplotlib.colormaps, which returns a colormap object. The length of the list of colors used internally to define the colormap can be adjusted via Colormap.resampled.
🌐
Matplotlib
matplotlib.org › 3.1.0 › tutorials › colors › colormap-manipulation.html
Creating Colormaps in Matplotlib — Matplotlib 3.1.0 documentation
May 18, 2019 - We can easily reduce the dynamic range of a colormap; here we choose the middle 0.5 of the colormap. However, we need to interpolate from a larger colormap, otherwise the new colormap will have repeated values.