2

I'm a climatologist and often plot anomalies of e.g. temperature fields using a "blue-to-white-to-red" colormap. To make the plots more readable, I discretize the colormap in a certain number of levels (bins) with a function which I "found" in the Internet (but I don't really understand it):

Something like this:

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib.colors as cols
from numpy.random import randn

def cmap_discretize(cmap, N):
    colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.)))
    colors_rgba = cmap(colors_i)
    indices = np.linspace(0, 1., N+1)
    cdict = {}
    for ki,key in enumerate(('red','green','blue')):
        cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) for i in xrange(N+1) ]
    # Return colormap object.
    return cols.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024)



cmap_disc= cmap_discretize(cm.RdBu_r,12)


fig, ax = plt.subplots()
data = np.clip(randn(250, 250), -1, 1)

cax = ax.pcolor(data, cmap=cmap_disc)
plt.colorbar(cax)

plt.show()

This results in

enter image description here

Now I want to set the two middle-most segments (i.e. those two close to 0) to white because I don't want to show very small deviations.

My goal would be to end up with something similar to this:

enter image description here

I really have a hard time to figure out how these LinearSegmentedColormap can be modified accordingly. Can somebody help me with this?

Raphael Roth
  • 26,751
  • 15
  • 88
  • 145
  • Please read: http://matplotlib.org/api/colors_api.html#matplotlib.colors.LinearSegmentedColormap which has a clear description of how the color maps work. – tacaswell Oct 05 '13 at 15:37
  • And you might get some traction using the code at http://stackoverflow.com/questions/15399095/stacking-colormaps/15399564#15399564 and passing in two descritized color maps. – tacaswell Oct 05 '13 at 15:43

2 Answers2

5

The function you have found builds a data structure (in cdict) for defining a LinearSegmentedColormap with segments that don't perform any interpolation (i.e., y1 in row i is always identical to y0 in row i+1, and this gives the constant, or discrete, color "bands").

cdict is a weird data structure, a dictionary which contains the keys 'red', 'green' and 'blue'. The value for each of these keys is a list structure containing tuples of the form (x, y0, y1). x is the color map coordinate, which is some floating point number between 0 and 1. y0 is the color value on the "left" side of x, and y1 is the color value on the "right" side of x. Colors are linearly interpolated in bands between consecutive values of x; if the first tuple is given by (0, A, B) and the second tuple by (X, C, D), then the color of a point t between 0 and X will be given by (t - 0) / (X - 0) * (C - B) + B.

For your purposes, your function works pretty well, but needs to have the "bands" near the middle of the color map replaced with a white color. You can try something like the following:

def cmap_discretize(cmap, N):
    colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.)))
    colors_rgba = cmap(colors_i)
    indices = np.linspace(0, 1., N+1)
    cdict = {}
    for ki,key in enumerate(('red','green','blue')):
        cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) for i in xrange(N+1) ]
    # "white out" the bands closest to the middle
    num_middle_bands = 2 - (N % 2)
    middle_band_start_idx = (N - num_middle_bands) // 2
    for middle_band_idx in range(middle_band_start_idx,
                                 middle_band_start_idx + num_middle_bands):
        for key in cdict.keys():
            old = cdict[key][middle_band_idx]
            cdict[key][middle_band_idx] = old[:2] + (1.,)
            old = cdict[key][middle_band_idx + 1]
            cdict[key][middle_band_idx + 1] = old[:1] + (1.,) + old[2:]
    # Return colormap object.
    return cols.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024)
wildwilhelm
  • 4,809
  • 1
  • 19
  • 24
3

Lets start by walking through the code you have

# get some uniformly sampled data, padded out a bit
colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.)))
# sample the input colormap at our sample points
colors_rgba = cmap(colors_i)
# indices for color map
indices = np.linspace(0, 1., N+1)
# dict to pass to the LinearSegmentedColormap
cdict = {}
# loop over the colors
for ki,key in enumerate(('red','green','blue')):
    # in each color assemble a list that looks like
    #[...,
    # (indices[2], colors_rgba[1,ki], colors_rgba[2,ki]),
    # (indices[3], colors_rgba[2,ki], colors_rgba[3,ki]),
    # ....]
    cdict[key] = [ (indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) for i in xrange(N+1) ]
    # The color for a number between [indices[2], indices[3]] are interpolated
    # between colors_rgba[2,ki] and colors_rgba[2,ki] which are the same
    # which is what gives you the discrete blocks.
# Construct and return colormap object.
return cols.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024)

So now the question is how to create a color map with a 'doubled' white band in the middle. I would change the function bit to have it take in two color maps (top and bottom)

import matplotlib.pyplot as plt
import numpy as np
from matplotlib import cm
import matplotlib.colors as cols
from numpy.random import randn

def cmap_double_discretize(cmap_bottom, cmap_top, N, split=.5):
    """
    Generates a descritized color map using two existing color maps

    Parameters
    ----------
    cmap_bottom : cmap
        The bottom cmap

    cmap_top : cmap
        The top cmap

    N : int
       The number of bins in each color map

    split : float, optional
       Where to join the maps, must be in [0, 1]
    """
    # sanity check
    assert split < 1 and split > 0
    # set up the data structure
    cdict = {lab: [] for lab in ('red','green','blue')}
    # do this in a fancy loop to a) save typing, b) make it easy to
    # retrofit to do arbitrary splits
    for cmap, ends in zip((cmap_bottom, cmap_top), ((0, split), (split, 1))):

        # run over the _whole_ range for each color map
        colors_i = np.concatenate((np.linspace(0, 1., N), (0.,0.,0.,0.)))
        # map the color
        colors_rgba = cmap(colors_i)
        # get the values 
        indices = np.linspace(ends[0], ends[1], N+1, endpoint=True)

        for ki,key in enumerate(('red','green','blue')):
            cdict[key].extend((indices[i], colors_rgba[i-1,ki], colors_rgba[i,ki]) for i in xrange(N+1))
            #    print cdict
    # Return colormap object.
    return cols.LinearSegmentedColormap(cmap.name + "_%d"%N, cdict, 1024)

red_cdict = {'red': [(0, 0, 1),
                     (1, 1, 0)],
            'blue': [(0, 0, 0),
                     (1, 1, 0)],
            'green': [(0, 0, 0),
                     (1, 1, 0)]}

blue_cdict = {'blue': [(0, 0, 1),
                       (1, 1, 0),],
            'red': [(0, 0, 1),
                    (1, 0, 0)],
            'green': [(0, 0, 1),
                     (1, 0, 0)]}
red_cmap = cols.LinearSegmentedColormap('red', red_cdict, 1024)
blue_cmap = cols.LinearSegmentedColormap('blue', blue_cdict, 1024)

test_cmap = cmap_double_discretize(red_cmap, blue_cmap, 6)
# these don't actually go to white!
# test_cmap = cmap_double_discretize(cm.get_cmap('Reds_r'), cm.get_cmap('Blues'), 6)


fig, ax = plt.subplots()
data = np.clip(randn(250, 250), -1, 1)

cax = ax.pcolor(data, cmap=test_cmap)
plt.colorbar(cax)

plt.show()

enter image description here

You can easily modify this to split across more than two color maps.

tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • this also works, but does not support predefined colormaps with white in the middle (like bwr,seismic,RdBu,RdGy...). Still many thanks for your effort – Raphael Roth Oct 05 '13 at 16:49