EStt EStt - 6 months ago 76
Python Question

Discrete then linear colormap in matplotlib

I have a matrix of data ranging from 0 to 102, and when using imshow i'd like to have 0 displayed as burnt red, 1 displayed as yellow-green and every other value from 2 to 102 in a linear gradient from standard green to dark green.

So far I've tried using the colormap 'Greens' and replacing the first two values but I has inconsistent results and I don't know how to use only half of the colormap.
I've also tried to generate my own colormap using a colordict and then overriding the first two entries but it also has inconsitent results depending on the data submitted (for example 1 displaying as burnt red)

Answer

You could use make_colormap to make a custom colormap:

mygreen = make_colormap([c('burnt red'), 
                         c('yellow green'), idx[1], c('yellow green'), 
                         (0,1,0), idx[2], (0,1,0),  # RGB (0,1,0) is standard green?
                         c('dark green')])
  • The argument to make_colormap is a sequence of RGB values and floats.

  • Every float is sandwiched between two RGB values.

  • The floats indicate locations (on a scale from 0.0 to 1.0) where the color map transitions from one RGB color to the next.

  • The first and last values in the sequence are the first and last colors in the color map.


import numpy as np
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
np.random.seed(2016)

def make_colormap(seq):
    """Return a LinearSegmentedColormap
    seq: a sequence of floats and RGB-tuples. 
      - Every float is sandwiched between two RGB values.
      - The floats indicate locations (on a scale from 0.0 to 1.0) where the
        color map transitions from one RGB color to the next.
      - The floats should be in increasing order
      - The first and last values in the sequence are the first and last
        colors in the color map.
    http://stackoverflow.com/q/16834861/190597 (unutbu)
    """
    seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
    cdict = {'red': [], 'green': [], 'blue': []}
    for i, item in enumerate(seq):
        if isinstance(item, float):
            r1, g1, b1 = seq[i - 1]
            r2, g2, b2 = seq[i + 1]
            cdict['red'].append([item, r1, r2])
            cdict['green'].append([item, g1, g2])
            cdict['blue'].append([item, b1, b2])
    return mcolors.LinearSegmentedColormap('CustomMap', cdict)

# There are 103 integers from 0 to 102 (inclusive)
idx = np.linspace(0, 1, 103)
c = mcolors.ColorConverter().to_rgb
mygreen = make_colormap([c('burnt red'), 
                         c('yellow green'), idx[1], c('yellow green'), 
                         (0,1,0), idx[2], (0,1,0),  # RGB (0,1,0) is standard green?
                         c('dark green')])

arr = np.random.randint(0, 103, size=(11, 11))
print(np.where(arr==0))
print(np.where(arr==1))
plt.imshow(arr, interpolation='nearest', cmap=mygreen, vmin=0, vmax=102)
plt.colorbar(ticks=list(range(0, 100, 10))+[102])
plt.show()

enter image description here

Another (probably better) option is to modify arr to take advantage of matplotlib's cmap.set_under and cmap.set_over methods. These colormap methods allow you to set a color for all values that go under or over specified limits:

cmap = make_colormap([(0,1,0), c('dark green')])
cmap.set_under('burnt red')
cmap.set_over('yellow green')

Thus, in the special case where you have just two special colors, you could modify arr so 0 gets mapped to, say, a negative number, and 1 gets mapped to a number larger than 100, and shift the rest of the numbers to range from 0 to 100:

arr -= 2   
arr = np.where(arr==-1, 102, arr)

For example,

import numpy as np
import matplotlib.colors as mcolors
import matplotlib.pyplot as plt
np.random.seed(2016)

def make_colormap(seq):
    """Return a LinearSegmentedColormap
    seq: a sequence of floats and RGB-tuples. 
      - Every float is sandwiched between two RGB values.
      - The floats indicate locations (on a scale from 0.0 to 1.0) where the
        color map transitions from one RGB color to the next.
      - The floats should be in increasing order
      - The first and last values in the sequence are the first and last
        colors in color map.
    http://stackoverflow.com/q/16834861/190597 (unutbu)
    """
    seq = [(None,) * 3, 0.0] + list(seq) + [1.0, (None,) * 3]
    cdict = {'red': [], 'green': [], 'blue': []}
    for i, item in enumerate(seq):
        if isinstance(item, float):
            r1, g1, b1 = seq[i - 1]
            r2, g2, b2 = seq[i + 1]
            cdict['red'].append([item, r1, r2])
            cdict['green'].append([item, g1, g2])
            cdict['blue'].append([item, b1, b2])
    return mcolors.LinearSegmentedColormap('CustomMap', cdict)

c = mcolors.ColorConverter().to_rgb
cmap = make_colormap([(0,1,0), c('dark green')])
cmap.set_under('burnt red')
cmap.set_over('yellow green')

arr = np.random.randint(0, 102, size=(11, 11))
# modify arr so that 0 is mapped to a negative number (-2) and 1 is mapped to a
# positive number greater than 100, (say, 102), and all other values are
# decreased by 2
arr -= 2   
arr = np.where(arr==-1, 102, arr)

plt.imshow(arr, interpolation='nearest', cmap=cmap, vmin=0, vmax=100)
plt.colorbar(extend='both')
plt.show()

enter image description here

Comments