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
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
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')
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:

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.
You can easily create a custom colormap using LinearSegmentedColormap and choosing the colors that you want (in this case, a subset of the original colormap)
min_val, max_val = 0.3,1.0
n = 10
orig_cmap = plt.cm.Blues
colors = orig_cmap(np.linspace(min_val, max_val, n))
cmap = matplotlib.colors.LinearSegmentedColormap.from_list("mycmap", colors)
demo
gradient = np.linspace(0, 1, 256)
gradient = np.vstack((gradient, gradient))
fig, (ax1, ax2) = plt.subplots(2,1, figsize=(5,2))
ax1.imshow(gradient, cmap=orig_cmap, aspect='auto')
ax1.set_title('original')
ax2.imshow(gradient, cmap=cmap, aspect='auto')
ax2.set_title('custom')
plt.tight_layout()

For future reference, my CMasher package provides a function that can make a sub-version of any colormap very easily, while also taking care of using enough color segments (https://cmasher.readthedocs.io/user/usage.html#sub-colormaps).
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:

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.





